Naver Login on React Native's New Architecture: What My TurboModule Rewrite Changes (and What It Doesn't)

If you've ever tried to move a real app onto React Native's New Architecture, you know the drill: you flip the flag, run the build, and then one dependency at a time reminds you it was written for the old Bridge. Naver Login was that dependency for me. So I rewrote it — react-native-naver-login-turbo — as a real TurboModule.
The tempting way to sell that would be "drop-in replacement." But I want to be precise about a word people throw around too easily, because a wrong reading of "drop-in" is exactly how a migration goes sideways at 11pm. Here's what actually carries over, what changes, and how to decide.
What "drop-in" really means here
react-native-naver-login-turbo preserves the main JavaScript call sites and response shapes for initialize(), login(), logout(), deleteToken(), and getProfile(). If your app already talks to Naver Login through those methods, most of that code survives the switch.
What it is not is a native-integration drop-in for every project. It changes the architecture contract, removes the upstream getAgreement() wrapper, removes the iOS AppDelegate URL-handler requirement, raises the platform floor, and adds a new refreshToken() method. So the honest positioning is: a New-Architecture-native successor with API compatibility for the common login surface — not a universal no-work replacement.
Where they diverge
The upstream package I'm comparing against is @react-native-seoul/naver-login. It's the compatibility baseline — a Bridge-style module that autolinks from RN 0.60 and doesn't impose a New-Architecture floor. Mine goes the other direction on purpose:
- Architecture: turbo declares
codegenConfig(spec nameNaverLoginSpec) and resolves the module throughTurboModuleRegistry.getEnforcing<Spec>('NaverLogin')instead ofNativeModules. Upstream still ships a Bridge-style module (NativeModules.RNNaverLogin), even though its podspec and Gradle carry conditional New-Arch interop. The precise distinction is "Bridge module with conditional New-Arch interop" versus "purpose-built codegen TurboModule." - Platform floor: turbo asks for RN
0.76+with New Architecture, iOS15.1+, and Android API24+with AGP8.x+. Most of that isn't a turbo tax — RN 0.76 is the release that defaults the New Architecture on and raises the iOS deployment target to 15.1, so you inherit those floors the moment you adopt 0.76. Upstream sits lower and older (iOS9.0, min SDK21). Both wrap Naver's Android OAuth SDK5.10.0— turbo's pin I confirmed in itsandroid/build.gradle; upstream's I'm taking from package metadata, so re-check it if it matters to you. - Expo: upstream ships a config plugin that mutates
Info.plistand AppDelegate URL handling. As of0.1.5, turbo publishes noapp.plugin.js(confirmed in-repo).
That first bullet is the whole story. Here's the actual spec turbo ships — a codegen TurboModule resolved eagerly, no NativeModules:
// src/NativeNaverLogin.ts
export interface Spec extends TurboModule {
initialize(params: NaverLoginInitParams): void;
login(): Promise<Object>;
refreshToken(): Promise<Object>; // added beyond upstream's surface
logout(): Promise<void>;
deleteToken(): Promise<void>;
getProfile(accessToken: string): Promise<Object>;
}
export default TurboModuleRegistry.getEnforcing<Spec>("NaverLogin");
(Complex results come back as Object from codegen and get cast to their real types in the JS wrapper, which keeps the spec codegen-safe.) Both packages are MIT. So the JavaScript stays stable; the native contract is where the real work is.
The API surface, honestly
For the login-token surface, the compatibility baseline is genuinely strong. Both packages use NaverLoginResponse with isSuccess, an optional successResponse (accessToken, refreshToken, expiresAtUnixSecondString, tokenType), and an optional failureResponse (message, isCancel, and Android-only SDK error-code fields).
One contract detail matters more than it looks: in turbo, login() resolves a Promise<NaverLoginResponse> and never rejects for a login failure — cancellation and failure come back as isSuccess: false. (That contract lives in turbo's native layer; the JS wrapper passes the native result straight through without a try/catch, so it won't turn a rejection into isSuccess: false for you. Upstream returns the same response shape, but if you lean on the never-reject guarantee, confirm it in whichever package you ship.) When you migrate, keep your existing isSuccess branching. Don't "clean it up" into a try/catch; that's not how this promise behaves.
Two methods break the symmetry:
getAgreement()— supported upstream, not ported to turbo.refreshToken()— added in turbo, absent upstream.
refreshToken() is the real win
This is the strongest product-level reason to move once you're already New-Architecture-ready. refreshToken() reissues an access token from the SDK's stored refresh-token state without forcing an interactive login. And it deliberately mirrors the login() contract: success resolves with the normal token bundle, while missing or expired refresh-token state resolves as isSuccess: false rather than throwing.
Why bother? Naver's own developer guide describes access tokens as expiring after expires_in seconds — the default called out is 3600 seconds, one hour — and documents refresh-token-based reissue. Upstream exposes the refresh token in the login() result but never gave you a first-class wrapper to use it. Now there's one.
getAgreement() is a migration trap, not a deprecation
Here's the one that'll bite you if you skim. @react-native-seoul/naver-login still exposes getAgreement(accessToken), implemented as a plain fetch() to https://openapi.naver.com/v1/nid/agreement. The endpoint is not deprecated — Naver still documents the service-terms agreement inquiry flow.
I just chose not to port it, because it's a niche REST-only helper that doesn't need native SDK wrapping. If your code calls NaverLogin.getAgreement(token), replace that wrapper with an app-owned API call — same Authorization: Bearer <accessToken> pattern you already use for profile calls — before you switch libraries. Do it in that order and it's a non-event.
The iOS migration is inverted from what you expect
Most native-library migrations tell you to add AppDelegate wiring. This one tells you to remove it.
Upstream's README requires an AppDelegate application:openURL:options: handler that forwards the callback URL to NaverThirdPartyLoginConnection (its Expo plugin injects the same thing). Turbo moves that callback handling inside the library by subscribing to RCTOpenURLNotification during initialization. So keep your URL scheme and LSApplicationQueriesSchemes entries — but remove the old Naver-specific AppDelegate forwarding, or you'll get duplicate/stale callback handling.
On Android, keep the OAuthLoginActivity and its ProGuard rule from the README unless you have a verified alternative:
<activity
android:name="com.navercorp.nid.oauth.OAuthLoginActivity"
android:theme="@android:style/Theme.Translucent.NoTitleBar" />
One gotcha: turbo's library AndroidManifest.xml is empty — no activity declarations at all, still true as of 0.1.5 — so don't assume the activity gets merged in for you. Declare it in the host app.
And if you're on Expo managed or a config-plugin-heavy setup: turbo doesn't publish a config plugin yet, so run it through expo prebuild / EAS before you trust the word "drop-in."
A migration checklist that won't surprise you
- Confirm you're on RN
0.76+with New Architecture enabled. - Swap the dependency and imports to
react-native-naver-login-turbo. - Keep existing
initialize(),login(),logout(),deleteToken(),getProfile()call sites initially. - Remove
getAgreement()or replace it with a direct Naver REST call. - Add
refreshToken()only after the baseline migration is green. - On iOS: keep
CFBundleURLTypesandLSApplicationQueriesSchemes, remove the old AppDelegateopenURLforwarding block. - On Android: verify API
24+, AGP8.x+,OAuthLoginActivity, and ProGuard/R8. - Test login success, cancellation, Naver-app-installed flow, WebView fallback, logout, delete-token failure, profile fetch, and token refresh.
So which one?
For a new app targeting the New Architecture, react-native-naver-login-turbo is the better default. For an existing app, migrate only when the platform floors and the architecture move are already acceptable — and stay on @react-native-seoul/naver-login if you still need the legacy Bridge, older RN/iOS/Android baselines, the upstream Expo config plugin, or getAgreement() and you don't want to own the REST call yet.
One honest caveat: I haven't run a neutral adoption or performance benchmark between the two. This is an architecture-and-API comparison, not a numbers shootout. If that's the deciding factor for you, measure it in your own app.
The biggest takeaway for me was the same one I keep relearning: "drop-in" is a JavaScript-call-site promise, not a native-integration promise. Get the native contract right — the AppDelegate removal, the empty manifest, the platform floors — and the JS really does mostly just work.
If you're already on the New Architecture, give it a try, and tell me what breaks.