Skip to content

Repository files navigation

naver-login-kmp — NAVER Login for Kotlin Multiplatform, a Kotlin Multiplatform library wrapping NAVER's native login SDKs

naver-login-kmp

Maven Central License Kotlin Platform Platform

NAVER Login (네이버 아이디로 로그인) for Kotlin Multiplatform. One shared API, NAVER's own native SDKs underneath — com.navercorp.nid:oauth on Android and NidThirdPartyLogin on iOS. Nothing about the login itself is reimplemented here: the app-to-app handoff to the NAVER app, the browser fallback when it is not installed, and the token storage are all NAVER's. Only the API you write against is shared.

NaverLogin.configure(
    NaverLoginConfig(clientId = "", clientSecret = "", clientName = "My App", urlScheme = "myapp"),
)

when (val result = NaverLogin.login()) {
    is NaverLoginResult.Success -> useToken(result.accessToken.value)
    NaverLoginResult.Cancelled -> Unit
    is NaverLoginResult.Failure -> showError(result.message)
}

✨ Why

  • Cancelling is not an error. Cancelled is its own result, so a user backing out of a login never reaches your error handling. Both SDKs report it as a failure code; this one does not.
  • Failures you can act on. NAVER's Android SDK reports twenty-odd string codes and its iOS SDK reports two enum cases with nested payloads. Both land on one NaverLoginErrorCode, with each SDK's own code and message passed through untouched on rawCode and rawMessage.
  • Nothing to add to your Android manifest. The proxy Activity this library needs is merged in for you, and NAVER's AAR merges its own permissions, <queries> entries and Activities.
  • No wrapper around your session. Both SDKs already persist and refresh their own tokens. This library adds no second, staler copy of that state.
  • "Not shared" is null, not "". NAVER returns an empty string for every profile field the user declined. An empty string is a value; null is the truth.
  • Concurrent logins are refused, not corrupted. NAVER's Android SDK keeps a single callback reference, so a second login quietly orphans the first. A second login() here returns LoginInProgress instead.
  • One line of Swift. Not a protocol to implement — see step 4.
  • A button you do not have to draw. NaverLoginButton is NAVER's own mark, wording and colours, measured off its published artwork. It is a separate artifact, so an app that does not use Compose never pays for it.

📋 Requirements

Component Requirement
Android minSdk 24 — this library's floor. NAVER's own AAR declares 21, so 24 is this project's choice, not NAVER's limit
iOS 13.0 — the floor NAVER's iOS SDK declares
Kotlin 2.4.0+
Xcode 16+
Compose Multiplatform 1.11.1 — only if you take the optional naverloginkmp-compose button. The core library has no UI dependency at all
NAVER SDK Android com.navercorp.nid:oauth:5.12.0, pulled in automatically · iOS naveridlogin-sdk-ios-swift 5.1.0, pulled in by this repository's Swift package

You also need an application registered on the NAVER Developers console, with 네이버 아이디로 로그인 enabled and one service environment per platform you ship. Step 0 walks through that form field by field, and says where each value it hands back belongs in your code.

🚀 Installation

0. Register your application with NAVER

Nothing below works without this, and it is where every value in NaverLoginConfig comes from.

Go to developers.naver.com/apps/#/register and sign in with a NAVER account. The registration form is one page.

애플리케이션 이름 is not an internal label: it is the name users read on the consent screen when they are asked to hand your app their profile. Under 사용 API, tick 네이버 아이디로 로그인 — that is the API this library drives, and the rest of the list is NAVER's other open APIs.

제공 정보 선택 — what your app may ever ask for

Choosing the API opens a list of profile items, each to be requested as a 필수 항목 or an 추가 항목. The consent screen presents the first group as required and the second as optional, so a user can decline an optional item and still sign in.

This list is a ceiling rather than a request: a field you did not tick here never arrives, whatever you call. 회원이름, 이메일, 별명 and 프로필사진 are the ordinary ones, available to any application. Anything past those four needs a separate approval from NAVER, and until you have it the field comes back empty for every user.

Console item Field on NaverProfile
회원이름 name
이메일 email
별명 nickname
프로필사진 profileImageUrl
성별 gender
연령대 age
생일 birthday
출생연도 birthYear
휴대전화번호 mobile

This is why every field on NaverProfile except id is nullable. A null there means one of three things and no code can tell them apart: the item was never selected here, NAVER has not approved your application for it, or the user declined it. id is the exception because NAVER returns it on every login.

로그인 오픈 API 서비스 환경 — one entry per platform

Add an environment for each platform you ship, and both if you ship both. Each asks for one field that the SDK actually uses:

  • Android패키지 이름, your application ID exactly: dev.yjyoon.naverloginkmp.sample for this repository's sample. NAVER identifies the Android app by package name and nothing else. It never asks for a signing certificate fingerprint, unlike Google Sign-In or Firebase — so there is no SHA-1 to paste, and no debug-versus-release pair to register.
  • iOSURL Scheme, not the bundle identifier. This catches out anyone who has registered an iOS app with another provider: NAVER never asks for the bundle ID. Whatever you enter has to be the same string in three places — this field, CFBundleURLSchemes in your Info.plist (step 3), and NaverLoginConfig.urlScheme. Pick something no other app will claim: when two apps claim one scheme iOS awards it to whichever it likes, and the failures that follow look random rather than like a configuration mistake.

서비스 URL and 다운로드 URL are for the console's own listing — a homepage and a store link. Neither SDK reads them.

What you get back

The application's overview page carries the two issued credentials. Those, plus the name you typed, are what the code needs:

From the console Where it goes Why it matters
Client ID NaverLoginConfig.clientId Identifies the application to NAVER.
Client Secret NaverLoginConfig.clientSecret Ships inside your app — see below.
애플리케이션 이름 NaverLoginConfig.clientName What users read on the consent screen. Keep it identical to the console: NAVER's mobile web login screen shows the registered name rather than this one, so a mismatch is visible to users.

The client secret is not optional, and it does not stay secret — both SDKs take it as an initialiser parameter, so it ends up in your APK and your IPA. There is one consequence you have to design your backend around, and it is written out in Your client secret ships inside your app.

With those four values — client ID, client secret, application name, and on iOS the URL scheme — the rest is installation.

1. Gradle

// shared/build.gradle.kts
kotlin {
    listOf(iosArm64(), iosSimulatorArm64()).forEach { iosTarget ->
        iosTarget.binaries.framework {
            baseName = "Shared"
            isStatic = true      // required — frameworks are dynamic by default
        }
    }
    sourceSets {
        commonMain.dependencies {
            implementation("dev.yjyoon.naverloginkmp:naverloginkmp:1.0.0")

            // Optional — NAVER's own login button, drawn to its guidelines.
            implementation("dev.yjyoon.naverloginkmp:naverloginkmp-compose:1.0.0")
        }
    }
}

naverloginkmp-compose is optional and additive: one composable, NaverLoginButton. It is a separate artifact because taking it means taking Compose Multiplatform, and an app built on Views or SwiftUI should not have to. It depends on the core artifact with api, so NaverLogin itself comes along with it. Its iOS targets are iosArm64 and iosSimulatorArm64 only — Compose Multiplatform publishes no Intel simulator target — while the core library also supports iosX64.

isStatic = true is not a preference. A dynamic framework has to resolve every symbol it references while Kotlin links it, which is long before Xcode has fetched the Swift package that defines them. A static one defers those symbols to your app's own link step, which is why step 2 happens in Xcode and not in Gradle.

2. iOS: add this repository as a Swift package

File ▸ Add Package Dependencies…https://github.com/yjyoon-dev/naver-login-kmp → choose the NaverLoginKmpShim product, and add it to your app target.

Pin it to the same version as the Gradle dependency — Exact Version 1.0.0. Releases are tagged v1.0.0; Swift Package Manager understands the v prefix, so type the version without it.

NaverLoginKmpShim depends on NAVER's own iOS SDK, so Swift Package Manager fetches naveridlogin-sdk-ios-swift for you and you do not add it yourself. Why the shim exists at all is explained below.

3. iOS: two Info.plist keys

<key>CFBundleURLTypes</key>
<array>
    <dict>
        <key>CFBundleTypeRole</key>
        <string>Editor</string>
        <key>CFBundleURLName</key>
        <string>naver-login</string>
        <key>CFBundleURLSchemes</key>
        <array>
            <string>myapp</string>
        </array>
    </dict>
</array>
<key>LSApplicationQueriesSchemes</key>
<array>
    <string>naversearchapp</string>
    <string>naversearchthirdlogin</string>
</array>

The scheme in CFBundleURLSchemes is your app's own — it is where NAVER returns after an app-to-app login, and the same string must appear as NaverLoginConfig.urlScheme and on the console. Pick something nobody else will claim: iOS gives a scheme to whichever app it feels like when two claim it, and the failure looks random rather than like a configuration mistake.

The two entries under LSApplicationQueriesSchemes are how the SDK asks whether the NAVER app is there. Omit them and canOpenURL answers false with no error anywhere: every user silently gets the browser flow, and isNaverAppInstalled() returns false on every device.

4. iOS: one line of Swift

import Shared   // your Kotlin framework, whatever you named it

ContentView()
    .onOpenURL { _ = NaverLoginUrlHandler.shared.handle(url: $0) }

NaverLoginUrlHandler is Kotlin, exported through your own shared framework — not something you import from NaverLoginKmpShim. The shim is a build-time dependency that supplies the symbols this library links against; nothing in your Swift code calls it directly.

That hands the callback URL back to NAVER's SDK, which is what finishes an app-to-app login. A SwiftUI App is scene-based and UIKit delivers URLs to the scene, so .onOpenURL is the right hook — AppDelegate.application(_:open:options:) is never called in an app shaped like that.

In a UIKit app, forward it from the app delegate instead:

func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any]) -> Bool {
    NaverLoginUrlHandler.shared.handle(url: url)
}

Android needs no manifest changes at all.

📖 Usage

Common

// Once, at startup — Application.onCreate on Android, your app's initialiser on iOS, or any shared
// initialisation that runs before the first login. It does not suspend.
NaverLogin.configure(
    NaverLoginConfig(
        clientId = BuildConfig.NAVER_CLIENT_ID,
        clientSecret = BuildConfig.NAVER_CLIENT_SECRET,
        clientName = "My App",
        urlScheme = "myapp",   // iOS only; ignored on Android, required on iOS
    ),
)

suspend fun signIn() {
    when (val result = NaverLogin.login()) {
        is NaverLoginResult.Success -> {
            val token = result.accessToken.value          // send this to your backend to verify
            val expiry = result.accessToken.expiresAtEpochMilliseconds
        }

        // The user changed their mind. Show nothing.
        NaverLoginResult.Cancelled -> Unit

        is NaverLoginResult.Failure -> when (result.code) {
            NaverLoginErrorCode.Network -> retryLater()
            NaverLoginErrorCode.NeedsNaverAppUpdate -> askUserToUpdateNaver()
            NaverLoginErrorCode.Authentication -> reportMisconfiguration(result.rawCode, result.message)
            else -> showError(result.message)
        }
    }
}

A successful login does not mean the user saw a screen. When a refresh token is stored, both SDKs renew the access token and return without showing anything.

Asking again for the profile fields a user declined:

NaverLogin.login(NaverLoginRequest(prompt = NaverLoginPrompt.RepromptPermissions))

Worth doing only when a field you genuinely need was refused. NAVER shows the consent screen again, and someone who said no once tends to notice being asked twice.

The profile, which is a separate network call because NAVER's login returns no profile of its own:

when (val result = NaverLogin.profile()) {
    is NaverProfileResult.Success -> {
        val id = result.profile.id                       // stable per application; key your accounts on this
        val email = result.profile.email                 // null when the user did not share it
    }
    is NaverProfileResult.Failure -> showError(result.message)
}

Signing out, disconnecting, and checking what this device holds:

NaverLogin.logout()          // drops the tokens on this device; the grant with NAVER survives
NaverLogin.disconnect()      // revokes the grant too — this is what account deletion needs

if (NaverLogin.isLoggedIn()) { /* a token exists here; it may still be expired */ }
val token: NaverAccessToken? = NaverLogin.currentAccessToken()

logout() and disconnect() are not the same operation, and the difference is visible to users: after logout() the connection is still listed on the user's NAVER security page, and the next login() completes without them typing anything. disconnect() reaches the network, so it can fail — and when it does, nothing was revoked.

Whether this device has the NAVER app, which decides the route a login takes. For deciding what to show; login() works either way:

if (NaverLogin.isNaverAppInstalled()) { /**/ }

Compose

:naverloginkmp-compose is the button, and it is the whole module:

NaverLoginButton(onClick = { scope.launch { handle(NaverLogin.login()) } })

NAVER's own N mark, NAVER's own wording for the reader's locale — 네이버 로그인 in Korean, Log in with Naver everywhere else — and the colours, corner radius, mark size and spacing measured off NAVER's published artwork.

The mark is NAVER's, taken from its official asset pack rather than redrawn, and it is a trademark: it is not covered by this project's Apache licence, and NAVER's brand guidelines apply to your application as much as to this one. NOTICE records exactly which file it came from and what was done to it.

@Composable
public fun NaverLoginButton(
    onClick: () -> Unit,
    modifier: Modifier = Modifier,
    enabled: Boolean = true,
    text: String? = NaverLoginButtonText.current(),
    style: NaverLoginButtonStyle = NaverLoginButtonStyle.Green,
    height: Dp = NaverLoginButtonDefaults.Height,
    shape: Shape = NaverLoginButtonDefaults.shape(iconOnly = text == null),
    textStyle: TextStyle = NaverLoginButtonDefaults.textStyle(height),
    naverIcon: Painter = NaverLoginButtonDefaults.naverIcon(),
    interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
)

style is Green or White — NAVER's two published treatments, both official and interchangeable. Each has a light and a dark form, and the button picks between them from isSystemInDarkTheme() rather than making you pass a third case, because the asset pack pairs them that way.

There is no colour parameter, and that is deliberate. NAVER's guidelines name an unspecified background colour, an unspecified label colour and a modified mark as violations, so the palette is not yours to override. Pressed, hovered and disabled states change the button's opacity instead — what lands on screen is still NAVER's own colour over your surface, and no fifth colour enters the palette. The four palettes are readable on NaverLoginButtonColors for anyone drawing their own button in UIKit or Views.

text = null gives the icon-only button, which the guidelines also permit: the default shape becomes a circle, and the mark carries NAVER's wording as its accessibility label, so a screen reader still announces what the button does rather than just "button". A label of your own has to still say that the button logs in with NAVER — NaverLoginButtonText.Published is the list NAVER itself offers.

height is the one number everything else is derived from: mark size, leading inset, label gap and text size are all ratios of it. NaverLoginButtonDefaults.MinHeight is 48 dp — the default height too — and below it the mark stops clearing the 18 px floor NAVER sets. The button grows past height if the reader's font-size setting demands it, rather than clipping the label.

Given no width the button wraps its content. Given a wider one — Modifier.fillMaxWidth(), a fixed width, a stretching parent — the mark keeps its square at the leading inset and the label stays centred in the button, which is how NAVER's own wide artwork is laid out. A label too long for the room beside the mark ellipsises rather than pushing the mark out of its square.

Android

Nothing to do. The application Context is picked up by an androidx.startup initialiser, so configure works from shared code, and the Activity the SDK needs is this library's own.

login() must be called while your app is in the foreground. Android does not allow starting an Activity from the background and gives no callback when it refuses, so a background call resolves to Failure(Internal, …) after a timeout rather than suspending forever.

iOS

Beyond the one line of Swift, one function exists on iOS only:

// iosMain
val result = NaverLogin.reauthenticate()

It forces a signed-in user to prove themselves again — for a payment screen, or before showing something sensitive. NAVER's Android SDK has no equivalent, so it is deliberately absent from NaverLoginPrompt rather than quietly doing something else on one platform.

🛠️ API

Type What it is
NaverLogin The entry point: configure, isConfigured, login, logout, disconnect, currentAccessToken, isLoggedIn, profile, isNaverAppInstalled
NaverLoginConfig Client ID, client secret, client name, and the iOS URL scheme
NaverLoginRequest Per-login options — currently the prompt
NaverLoginPrompt Default · RepromptPermissions
NaverLoginResult Success · Cancelled · Failure
NaverLogoutResult Success · Failure — no Cancelled, because there is nothing to cancel
NaverProfileResult Success · Failure
NaverLoginErrorCode NotConfigured Network Server Authentication NoAuthenticationApp NeedsNaverAppUpdate LoginInProgress Internal
NaverAccessToken Token value and expiry, in epoch milliseconds
NaverProfile id, nickname, name, email, gender, age, birthday, birthYear, profileImageUrl, mobile

From dev.yjyoon.naverloginkmp:naverloginkmp-compose, if you take it:

Type What it is
NaverLoginButton The composable — see Compose
NaverLoginButtonStyle Green · White
NaverLoginButtonText NAVER's published labels, plus of(languageTag) and the @Composable current() the button defaults to
NaverLoginButtonColors The four palettes — light and dark for each style — exposed for buttons this library does not draw
NaverLoginButtonDefaults Height, MinHeight, CornerRadius, BorderWidth, markSize, leadingInset, labelGap, shape, textStyle, naverIcon

The result model

login() never throws. Every outcome is a value:

public sealed interface NaverLoginResult {
    public class Success(
        public val accessToken: NaverAccessToken,
        public val refreshToken: String? = null,
    ) : NaverLoginResult

    public data object Cancelled : NaverLoginResult

    public class Failure(
        public val code: NaverLoginErrorCode,
        public val message: String,
        public val rawCode: String? = null,      // the native SDK's own code, verbatim
        public val rawMessage: String? = null,
    ) : NaverLoginResult
}

The only exception that ever escapes is CancellationException, when your own coroutine is cancelled.

Success carries tokens and nothing else: NAVER's login returns no profile, and this library will not make a network call you did not ask for. Call NaverLogin.profile() when you want one.

Every field on NaverProfile except id is nullable, and a null there means the user declined it on the consent screen or your application was never approved for it — not that the request failed. id is NAVER's identifier for that person within your application; the same person gets a different one in a different application, and their real NAVER ID is never disclosed.

NaverAccessToken.expiresAtEpochMilliseconds is in milliseconds. Both native SDKs report the expiry in seconds; the conversion happens here so the value matches every other timestamp in Kotlin.

⚖️ Two things this library cannot hide

Your client secret ships inside your app

NAVER requires the client secret in the app, on both platforms. It is a parameter of the native SDKs' own initialisers and there is no client-side flow that omits it, so it ends up in your APK and your IPA where anyone willing to unzip them can read it. Treat it as public.

The consequence that matters: a NAVER access token obtained on a device proves nothing to your server. Anyone holding the secret can mint tokens for your client ID. Send the token to your backend and verify it against NAVER there — https://openapi.naver.com/v1/nid/me with the token as a bearer credential tells you who it belongs to — and trust the answer NAVER gives your server, never the one the client sent.

Neither this library nor NAVER's own SDKs can fix this. It is written down because the alternative is letting people believe a secret shipped in an app is a secret.

The iOS half needs a Swift package from this repository

NAVER's iOS SDK is pure Swift, and not one symbol in it is @objc. Kotlin/Native's interop goes through Objective-C only, so cinterop cannot see the SDK at all — there is nothing to bind to.

NaverLoginKmpShim is this repository's answer: a small Swift package that wraps the parts of NidOAuth this library uses in explicit @objc declarations, which Kotlin then binds through cinterop as if they were an Objective-C SDK. It is source in swift/, published from the same repository and tagged with the same versions, so the Kotlin artifact and the Swift package can never drift apart. The cost is the one step you cannot skip: adding that SPM product to your app target, so the symbols the static Kotlin framework left undefined have something to resolve against.

🩺 Troubleshooting

Keyed by what you actually see.

Symptom Cause
Undefined symbols: _OBJC_CLASS_$_NLKNaverLogin when the app links The NaverLoginKmpShim product is not on the app target, or your Kotlin framework is dynamic — set isStatic = true, which is not the default.
cannot find 'NaverLoginUrlHandler' in scope That type is Kotlin, not Swift: import your own shared framework, the one whose baseName you set in binaries.framework. It is not part of NaverLoginKmpShim.
The NAVER app finishes the login and your app never comes back The URL is not being forwarded — .onOpenURL is not wired up, or CFBundleURLSchemes does not contain the exact string you passed as urlScheme.
Login always opens the browser even though NAVER is installed, and isNaverAppInstalled() is false everywhere naversearchapp and naversearchthirdlogin are missing from LSApplicationQueriesSchemes. Nothing errors; canOpenURL returns false.
Failure(Authentication, …) on every attempt, for every user The console does not match this build: wrong client ID or secret, an Android 패키지 이름 that is not this app's application ID, or a URL scheme that differs from the registered one. Step 0 is where all three come from.
Failure(NotConfigured, …) configure was never called, or it ran after the first login(). It is cheap and idempotent — call it at startup.
IllegalStateException: NaverLogin is already configured with client … configure was called twice with different values. Both SDKs are one-client-per-process, so this fails loudly rather than differing per platform.
Failure(LoginInProgress, …) A login is already running. Concurrent logins are refused rather than queued — usually a double tap on the sign-in button.
Failure(Internal, …) on Android with nothing on screen login() was called while the app was in the background.
profile() returns Failure(Authentication, …) The stored token expired. Call login() once to renew it and retry once — not in a loop.
Every profile field except id is null The items were never selected under 제공 정보 선택, your application is not approved for them, or the user declined them. NaverLoginPrompt.RepromptPermissions asks again; the other two you fix on the console.
Failure(NeedsNaverAppUpdate, …) The installed NAVER app is too old for this flow. Nothing to fix in your build; tell the user.
naverloginkmp-compose will not resolve for an iosX64 target Compose Multiplatform publishes no Intel simulator target, so this module has none either. The core artifact does — the button is the only part you lose.
Unknown iOS simulator arch: 'x86_64' An Intel simulator slice was requested for a project without an iosX64 target. Add EXCLUDED_ARCHS[sdk=iphonesimulator*] = x86_64.

🤔 Design notes

Why an Android proxy Activity. NAVER's requestLogin(context, callback) ends in a bare context.startActivity(intent) with no FLAG_ACTIVITY_NEW_TASK, so handing it an application Context throws AndroidRuntimeException. Shared code has no Activity of its own, so this library ships an invisible one that passes itself as the context — which is the usage NAVER documents.

Why configure does not suspend. Apps initialise things in Application.onCreate and in app initialisers, and neither is a coroutine. Both SDKs set themselves up asynchronously, so this library runs that setup on first use and makes every entry point wait for it. There is no window in which a login fails merely for being early.

Why NaverLogin is an object. Both SDKs are process-wide singletons whose setup is one-shot, and the iOS one reports a second initialisation as an error. An API that let you construct several clients would be lying about what the platforms can do.

Why there are no scopes. NAVER has no per-login scope parameter. Which fields your application may ask for is set on the console and granted by the user on the consent screen, so the only lever at call time is NaverLoginPrompt.RepromptPermissions.

Why login returns no profile. NAVER's login gives you tokens; the profile is a separate HTTP call. Making it automatically would spend a request, and the user's patience, on data most sign-in screens never read.

Why reauthenticate() is iOS-only. NAVER's iOS SDK has it and its Android SDK does not. Putting it in NaverLoginPrompt would mean one platform silently doing something else, so it lives in iosMain where its absence elsewhere is a compile error rather than a surprise.

Why empty strings become null. NAVER's profile response uses "" for every field the user did not share. Passing that through would make "declined" indistinguishable from "blank", and every consumer would write the same takeIf { it.isNotEmpty() }.

📱 Sample

sample/ is a Compose Multiplatform app running on both platforms, and the thing that proves a real app can still link. Put your own client details in local.properties at the repository root — it is gitignored, because a client secret in a public repository is not a secret:

naverClientId=your-client-id
naverClientSecret=your-client-secret
naverClientName=naver-login-kmp sample
naverUrlScheme=naverloginkmpsample

The build turns those into SAMPLE_NAVER_* constants in commonMain, so both entry points read the same values. Three sources are consulted, in this order: a Gradle property (-PnaverClientId=…, or ORG_GRADLE_PROJECT_naverClientId in CI), then local.properties, then an environment variable (NAVER_CLIENT_ID, NAVER_CLIENT_SECRET, NAVER_CLIENT_NAME, NAVER_URL_SCHEME). The property wins, so a command line can override the file; the environment variable is the last resort, for a machine with no file on disk. Leave them all out and every call answers NotConfigured rather than failing the build.

Your application on the NAVER Developers console has to declare what this sample actually ships:

Console field Value for this sample
Android 패키지 이름 dev.yjyoon.naverloginkmp.sample
iOS URL Scheme naverloginkmpsample (already in sample/iosApp/iosApp/Info.plist)

The sample's iOS bundle identifier is whatever you set in sample/iosApp/Configuration/Config.xcconfig, and it is registered nowhere: NAVER identifies an iOS app by URL scheme.

Then:

./gradlew :sample:composeApp:installDebug          # Android
open sample/iosApp/iosApp.xcodeproj                # iOS

📚 API documentation

Generated with Dokka and published to https://yjyoon-dev.github.io/naver-login-kmp/ on every release.

🤝 Contributing

Issues and pull requests are welcome — see CONTRIBUTING.md for how the project is laid out, which parts look wrong but are not, and what CI checks.

📄 License

Copyright 2026 yjyoon

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    https://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

NAVER, 네이버, "네이버 아이디로 로그인" and the NAVER "N" mark are trademarks of NAVER Corporation. The mark drawn by naverloginkmp-compose comes from NAVER's official button asset pack, is not covered by the Apache licence above, and is not licensed to you by this project — NOTICE records the file it came from, what was done to it, and the terms it is included under. NAVER's brand guidelines apply to your application as much as to this one. This is an independent open-source project and is not affiliated with, endorsed by, or sponsored by NAVER Corporation.

About

NAVER Login for Kotlin multiplatform

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages