diff --git a/android/build.gradle b/android/build.gradle
index 6ae1341..6c18a87 100755
--- a/android/build.gradle
+++ b/android/build.gradle
@@ -1,5 +1,5 @@
buildscript {
- def kotlin_version = rootProject.ext.has("kotlinVersion") ? rootProject.ext.get("kotlinVersion") : "1.9.24"
+ def kotlin_version = rootProject.ext.has("kotlinVersion") ? rootProject.ext.get("kotlinVersion") : "2.1.20"
repositories {
google()
@@ -13,10 +13,6 @@ buildscript {
}
}
-def isNewArchitectureEnabled() {
- return rootProject.hasProperty("newArchEnabled") && rootProject.getProperty("newArchEnabled") == "true"
-}
-
def safeExtGet(prop, fallback) {
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
}
@@ -26,34 +22,25 @@ apply plugin: "com.facebook.react"
apply plugin: "kotlin-android"
android {
- compileSdkVersion safeExtGet("compileSdkVersion", 35)
+ namespace "com.xmartlabs.line"
+ compileSdk safeExtGet("compileSdkVersion", 36)
defaultConfig {
- minSdkVersion safeExtGet("minSdkVersion", 24)
- targetSdkVersion safeExtGet("targetSdkVersion", 35)
+ minSdk safeExtGet("minSdkVersion", 24)
+ targetSdk safeExtGet("targetSdkVersion", 36)
versionCode 1
versionName "5.6.0"
}
sourceSets {
main {
- java.srcDirs += ["${project.buildDir}/generated/source/codegen/java"]
+ java.srcDirs += ["${layout.buildDirectory.get().asFile}/generated/source/codegen/java"]
}
}
}
dependencies {
implementation "com.facebook.react:react-native:+"
- implementation "com.linecorp.linesdk:linesdk:5.11.0"
- implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3"
+ implementation "com.linecorp.linesdk:linesdk:5.12.0"
+ implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.10.2"
}
-
-tasks.register("checkNativeLineLoginSpec") {
- doLast {
- if (!isNewArchitectureEnabled()) {
- throw new GradleException("@xmartlabs/react-native-line v5 requires your project to have NEW ARCHITECTURE ENABLED. Use v4 if you want to keep using the old architecture.")
- }
- }
-}
-
-preBuild.dependsOn checkNativeLineLoginSpec
diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml
index 84f5483..94cbbcf 100755
--- a/android/src/main/AndroidManifest.xml
+++ b/android/src/main/AndroidManifest.xml
@@ -1,2 +1 @@
-
+
diff --git a/android/src/main/java/com/xmartlabs/line/LineLoginModule.kt b/android/src/main/java/com/xmartlabs/line/LineLoginModule.kt
index 106313e..e5bb54c 100755
--- a/android/src/main/java/com/xmartlabs/line/LineLoginModule.kt
+++ b/android/src/main/java/com/xmartlabs/line/LineLoginModule.kt
@@ -1,8 +1,9 @@
package com.xmartlabs.line
import android.app.Activity
-import android.content.Context
import android.content.Intent
+import java.util.concurrent.atomic.AtomicBoolean
+import java.util.concurrent.atomic.AtomicReference
import com.facebook.react.bridge.*
import com.facebook.react.module.annotations.ReactModule
@@ -14,298 +15,280 @@ import com.linecorp.linesdk.auth.LineAuthenticationConfig
import com.linecorp.linesdk.auth.LineAuthenticationParams
import com.linecorp.linesdk.auth.LineLoginApi
import com.linecorp.linesdk.auth.LineLoginResult
-import com.linecorp.linesdk.LineProfile
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
-private var LOGIN_REQUEST_CODE: Int = 0
-
-enum class LoginArguments(val key: String) {
- BOT_PROMPT("botPrompt"),
- ONLY_WEB_LOGIN("onlyWebLogin"),
- SCOPES("scopes")
-}
-
-class LineLoginModule(private val reactContext: ReactApplicationContext) :
+@ReactModule(name = LineLoginModule.NAME)
+class LineLoginModule(reactContext: ReactApplicationContext) :
NativeLineLoginSpec(reactContext) {
companion object {
+ const val BOT_PROMPT = "botPrompt"
+ const val CHANNEL_ID = "channelId"
+ const val ONLY_WEB_LOGIN = "onlyWebLogin"
+ const val SCOPES = "scopes"
+
const val NAME = NativeLineLoginSpec.NAME
+ private const val LOGIN_REQUEST_CODE = 1423
+ private val DEFAULT_SCOPES = listOf("profile")
}
- private val coroutineScope: CoroutineScope = CoroutineScope(Dispatchers.Main)
+ private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
+ private val pendingLogin = AtomicReference(null)
- private lateinit var channelId: String
- private lateinit var lineApiClient: LineApiClient
- private var loginResult: Promise? = null
+ @Volatile private var channelId: String? = null
+ @Volatile private var lineApiClient: LineApiClient? = null
- override fun setup(args: ReadableMap, promise: Promise) {
- val channelIdArg = args.getString("channelId")
- if (channelIdArg == null) {
- return promise.reject("SETUP_FAILED", "channelId is required", null)
- }
- channelId = channelIdArg
- lineApiClient = LineApiClientBuilder(reactContext.applicationContext, channelId).build()
- try {
- reactContext.addActivityEventListener(object : ActivityEventListener {
- override fun onNewIntent(intent: Intent) {}
- override fun onActivityResult(
- activity: Activity,
- requestCode: Int,
- resultCode: Int,
- data: Intent?
- ) =
- handleActivityResult(requestCode, resultCode, data)
- })
- promise.resolve(null)
- } catch (e: Throwable) {
- promise.reject("SETUP_FAILED", e.message, e)
+ private val activityListenerRegistered = AtomicBoolean(false)
+
+ override fun invalidate() {
+ scope.cancel()
+ pendingLogin.getAndSet(null)
+ ?.reject("MODULE_INVALIDATED", "Module was destroyed during login", null)
+ if (activityListenerRegistered.getAndSet(false)) {
+ reactApplicationContext.removeActivityEventListener(loginResultListener)
}
+ super.invalidate()
}
- override fun login(args: ReadableMap, promise: Promise) {
- val scopes =
- if (args.hasKey(LoginArguments.SCOPES.key)) args.getArray(LoginArguments.SCOPES.key)!!
- .toArrayList() as List else listOf("profile")
- val onlyWebLogin =
- args.hasKey(LoginArguments.ONLY_WEB_LOGIN.key) && args.getBoolean(LoginArguments.ONLY_WEB_LOGIN.key)
- val botPromptString =
- if (args.hasKey(LoginArguments.BOT_PROMPT.key)) args.getString(LoginArguments.BOT_PROMPT.key)!! else "normal"
- login(
- scopes,
- onlyWebLogin,
- botPromptString,
- promise
- )
- }
+ override fun setup(args: ReadableMap, promise: Promise) {
+ val id = args.getString(CHANNEL_ID)?.takeIf { it.isNotBlank() }
+ ?: return promise.reject("SETUP_FAILED", "channelId must be a non-empty string", null)
- private fun login(
- scopes: List,
- onlyWebLogin: Boolean,
- botPromptString: String,
- promise: Promise
- ) {
+ channelId = id
- val lineAuthenticationParams = LineAuthenticationParams.Builder()
- .scopes(Scope.convertToScopeList(scopes))
- .apply {
- botPrompt(LineAuthenticationParams.BotPrompt.valueOf(botPromptString))
- }
- .build()
+ lineApiClient = LineApiClientBuilder(reactApplicationContext, id).build()
- val lineAuthenticationConfig: LineAuthenticationConfig? =
- createLineAuthenticationConfig(channelId, onlyWebLogin)
+ if (activityListenerRegistered.compareAndSet(false, true)) {
+ reactApplicationContext.addActivityEventListener(loginResultListener)
+ }
+ promise.resolve(null)
+ }
- val activity: Activity = currentActivity!!
+ override fun login(args: ReadableMap, promise: Promise) {
+ requireClient(promise) ?: return
- val loginIntent =
- when {
- lineAuthenticationConfig != null -> LineLoginApi.getLoginIntent(
- activity,
- lineAuthenticationConfig,
- lineAuthenticationParams
- )
+ val id = channelId ?: return promise.reject("NOT_SETUP", "Call setup() first", null)
- onlyWebLogin -> LineLoginApi.getLoginIntentWithoutLineAppAuth(
- activity, channelId, lineAuthenticationParams
- )
+ val scopes = args.getArray(SCOPES)
+ ?.toArrayList()?.filterIsInstance()?.takeIf { it.isNotEmpty() }
+ ?: DEFAULT_SCOPES
- else -> LineLoginApi.getLoginIntent(activity, channelId, lineAuthenticationParams)
- }
+ val onlyWebLogin = args.hasKey(ONLY_WEB_LOGIN) && args.getBoolean(ONLY_WEB_LOGIN)
- activity.startActivityForResult(loginIntent, LOGIN_REQUEST_CODE)
- this.loginResult = promise
- }
+ val botPromptRaw = args.getString(BOT_PROMPT) ?: "normal"
+ val botPrompt = LineAuthenticationParams.BotPrompt.entries
+ .find { it.name.equals(botPromptRaw, ignoreCase = true) }
+ ?: return promise.reject(
+ "INVALID_ARGUMENT",
+ "Invalid botPrompt '$botPromptRaw'. Expected: ${
+ LineAuthenticationParams.BotPrompt.entries.joinToString { it.name.lowercase() }
+ }",
+ null,
+ )
- override fun getProfile(promise: Promise) {
- coroutineScope.launch {
- val lineApiResponse = withContext(Dispatchers.IO) { lineApiClient.getProfile() }
- if (!lineApiResponse.isSuccess) {
- promise.reject(
- lineApiResponse.responseCode.name,
- lineApiResponse.errorData.message,
- null
- )
- } else {
- promise.resolve(parseProfile(lineApiResponse.responseData))
- }
+ val activity = currentActivity
+ ?: return promise.reject("NO_ACTIVITY", "Activity is not available", null)
+
+ if (!pendingLogin.compareAndSet(null, promise)) {
+ return promise.reject("LOGIN_IN_PROGRESS", "A login is already in progress", null)
}
- }
- fun handleActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
- if (requestCode != LOGIN_REQUEST_CODE) return
+ val authParams = LineAuthenticationParams.Builder()
+ .scopes(Scope.convertToScopeList(scopes))
+ .botPrompt(botPrompt)
+ .build()
+
+ val config = if (onlyWebLogin) {
+ LineAuthenticationConfig.Builder(id).disableLineAppAuthentication().build()
+ } else null
- if (resultCode != Activity.RESULT_OK || intent == null) {
- loginResult?.reject(
- resultCode.toString(),
- "ERROR",
- null
- )
+ val intent = if (config != null) {
+ LineLoginApi.getLoginIntent(activity, config, authParams)
+ } else {
+ LineLoginApi.getLoginIntent(activity, id, authParams)
}
- val result = LineLoginApi.getLoginResultFromIntent(intent)
-
- when (result.responseCode) {
- LineApiResponseCode.SUCCESS -> {
- loginResult?.resolve(parseLoginResult(result))
- loginResult = null
- }
+ activity.startActivityForResult(intent, LOGIN_REQUEST_CODE)
+ }
- LineApiResponseCode.CANCEL -> {
- loginResult?.reject(
- result.responseCode.name,
- result.errorData.message,
- null
- )
+ private val loginResultListener = object : ActivityEventListener {
+ override fun onNewIntent(intent: Intent) = Unit
+
+ override fun onActivityResult(
+ activity: Activity,
+ requestCode: Int,
+ resultCode: Int,
+ data: Intent?
+ ) {
+ if (requestCode != LOGIN_REQUEST_CODE) return
+ val promise = pendingLogin.getAndSet(null) ?: return
+
+ if (resultCode != Activity.RESULT_OK || data == null) {
+ val (code, message) = if (resultCode == Activity.RESULT_CANCELED) {
+ "LOGIN_CANCELLED" to "Login was cancelled by the user"
+ } else {
+ "LOGIN_FAILED" to "Login failed with unexpected result code $resultCode"
+ }
+ return promise.reject(code, message, null)
}
- else -> {
- loginResult?.reject(
+ val result = LineLoginApi.getLoginResultFromIntent(data)
+ if (result.responseCode == LineApiResponseCode.SUCCESS) {
+ val cred = result.lineCredential
+ val prof = result.lineProfile
+ if (cred != null && prof != null) {
+ promise.resolve(buildLoginResult(result, cred, prof))
+ } else {
+ promise.reject(
+ "PARSE_ERROR",
+ "Credential or profile missing from login result",
+ null
+ )
+ }
+ } else {
+ promise.reject(
result.responseCode.name,
- result.errorData.message,
+ result.errorData.message ?: result.responseCode.name,
null
)
}
}
-
- loginResult = null
}
override fun logout(promise: Promise) {
- coroutineScope.launch {
- val lineApiResponse = withContext(Dispatchers.IO) { lineApiClient.logout() }
- if (lineApiResponse.isSuccess) {
- promise.resolve(null)
- } else {
- promise.reject(
- lineApiResponse.responseCode.name,
- lineApiResponse.errorData.message,
+ val client = requireClient(promise) ?: return
+ scope.launch {
+ try {
+ val r = client.logout()
+ if (r.isSuccess) promise.resolve(null)
+ else promise.reject(
+ r.responseCode.name,
+ r.errorData.message ?: r.responseCode.name,
null
)
+ } catch (e: Exception) {
+ promise.reject("API_ERROR", e.message ?: "Unexpected error during logout", e)
}
}
}
- override fun getCurrentAccessToken(promise: Promise) = invokeLineServiceMethod(
- promise = promise,
- serviceCallable = { lineApiClient.getCurrentAccessToken() },
- parser = { parseAccessToken(it, lineIdToken = null) }
+ override fun getProfile(promise: Promise) = apiCall(
+ promise,
+ call = LineApiClient::getProfile,
+ build = ::buildProfile,
+ )
+
+ override fun getCurrentAccessToken(promise: Promise) = apiCall(
+ promise,
+ call = LineApiClient::getCurrentAccessToken,
+ build = { buildAccessToken(it, idToken = null) },
)
- override fun getFriendshipStatus(promise: Promise) = invokeLineServiceMethod(
- promise = promise,
- serviceCallable = { lineApiClient.getFriendshipStatus() },
- parser = { parseFriendshipStatus(it) }
+ override fun getFriendshipStatus(promise: Promise) = apiCall(
+ promise,
+ call = LineApiClient::getFriendshipStatus,
+ build = ::buildFriendshipStatus,
)
- override fun refreshAccessToken(promise: Promise) = invokeLineServiceMethod(
- promise = promise,
- serviceCallable = { lineApiClient.refreshAccessToken() },
- parser = { parseAccessToken(it, lineIdToken = null) }
+ override fun refreshAccessToken(promise: Promise) = apiCall(
+ promise,
+ call = LineApiClient::refreshAccessToken,
+ build = { buildAccessToken(it, idToken = null) },
)
- override fun verifyAccessToken(promise: Promise) = invokeLineServiceMethod(
- promise = promise,
- serviceCallable = { lineApiClient.verifyToken() },
- parser = { parseVerifyAccessToken(it) }
+ override fun verifyAccessToken(promise: Promise) = apiCall(
+ promise,
+ call = LineApiClient::verifyToken,
+ build = ::buildVerifyResult,
)
- private fun invokeLineServiceMethod(
+ /**
+ * Returns the initialized [LineApiClient] or rejects [promise] with NOT_SETUP
+ * and returns null, allowing callers to use the `?: return` pattern.
+ */
+ private fun requireClient(promise: Promise): LineApiClient? =
+ lineApiClient ?: run {
+ promise.reject("NOT_SETUP", "Call setup() before using the LINE SDK", null)
+ null
+ }
+
+ /**
+ * Runs a blocking LINE API [call] on the IO dispatcher.
+ * Resolves [promise] via [build] on success, or rejects it with the LINE error.
+ */
+ private fun apiCall(
promise: Promise,
- serviceCallable: () -> LineApiResponse,
- parser: (T) -> WritableMap
+ call: (LineApiClient) -> LineApiResponse,
+ build: (T) -> WritableMap,
) {
- coroutineScope.launch {
- val lineApiResponse = withContext(Dispatchers.IO) { serviceCallable.invoke() }
- if (lineApiResponse.isSuccess) {
- promise.resolve(parser.invoke(lineApiResponse.responseData))
- } else {
- promise.reject(
- lineApiResponse.responseCode.name,
- lineApiResponse.errorData.message,
+ val client = requireClient(promise) ?: return
+ scope.launch {
+ try {
+ val r = call(client)
+ if (r.isSuccess) promise.resolve(build(r.responseData))
+ else promise.reject(
+ r.responseCode.name,
+ r.errorData.message ?: r.responseCode.name,
null
)
+ } catch (e: Exception) {
+ promise.reject("API_ERROR", e.message ?: "Unexpected error", e)
}
}
}
-
- private fun parseAccessToken(
- accessToken: LineAccessToken,
- lineIdToken: LineIdToken?
- ): WritableMap = Arguments.makeNativeMap(
- mapOf(
- "accessToken" to accessToken.tokenString,
- "expiresIn" to accessToken.expiresInMillis,
- "idToken" to lineIdToken?.rawString
- )
- )
-
- private fun parseFriendshipStatus(friendshipStatus: LineFriendshipStatus): WritableMap =
+ /**
+ * [expiresIn] is seconds-until-expiry (OAuth standard `expires_in`),
+ * derived from the SDK's millisecond value.
+ */
+ private fun buildAccessToken(token: LineAccessToken, idToken: LineIdToken?): WritableMap =
Arguments.makeNativeMap(
mapOf(
- "friendFlag" to friendshipStatus.isFriend
+ "accessToken" to token.tokenString,
+ "expiresIn" to token.expiresInMillis / 1000L,
+ "idToken" to idToken?.rawString,
)
)
- private fun parseProfile(profile: LineProfile): WritableMap = Arguments.makeNativeMap(
- mapOf(
- "displayName" to profile.displayName,
- "pictureUrl" to profile.pictureUrl?.toString(),
- "statusMessage" to profile.statusMessage,
- "userId" to profile.userId
+ private fun buildProfile(profile: LineProfile): WritableMap =
+ Arguments.makeNativeMap(
+ mapOf(
+ "displayName" to profile.displayName,
+ "pictureUrl" to profile.pictureUrl?.toString(),
+ "statusMessage" to profile.statusMessage,
+ "userId" to profile.userId,
+ )
)
- )
- private fun parseLoginResult(loginResult: LineLoginResult): WritableMap =
+ private fun buildFriendshipStatus(status: LineFriendshipStatus): WritableMap =
Arguments.makeNativeMap(
mapOf(
- "accessToken" to parseAccessToken(
- loginResult.lineCredential!!.accessToken,
- loginResult.lineIdToken
- ),
- "friendshipStatusChanged" to loginResult.friendshipStatusChanged,
- "idTokenNonce" to loginResult.lineIdToken?.nonce,
- "scope" to loginResult.lineCredential?.scopes?.let {
- Scope.join(it)
- },
- "userProfile" to parseProfile(loginResult.lineProfile!!)
+ "friendFlag" to status.isFriend
)
)
- private fun parseVerifyAccessToken(verifyAccessToken: LineCredential): WritableMap =
+ private fun buildLoginResult(result: LineLoginResult, credential: LineCredential, profile: LineProfile): WritableMap =
Arguments.makeNativeMap(
mapOf(
- "clientId" to channelId,
- "expiresIn" to verifyAccessToken.accessToken.expiresInMillis,
- "scope" to Scope.join(verifyAccessToken.scopes)
+ "accessToken" to buildAccessToken(credential.accessToken, result.lineIdToken),
+ "friendshipStatusChanged" to result.friendshipStatusChanged,
+ "idTokenNonce" to result.lineIdToken?.let { result.nonce },
+ "scope" to Scope.join(credential.scopes),
+ "userProfile" to buildProfile(profile),
)
)
-
- private fun createConfig(
- channelId: String,
- isLineAppAuthDisabled: Boolean
- ): LineAuthenticationConfig {
- val configBuilder = LineAuthenticationConfig.Builder(channelId)
-
- if (isLineAppAuthDisabled) {
- configBuilder.disableLineAppAuthentication()
- }
-
- return configBuilder.build()
- }
-
- private fun createLineAuthenticationConfig(
- channelId: String,
- onlyWebLogin: Boolean
- ): LineAuthenticationConfig? {
- return createConfig(
- channelId,
- onlyWebLogin
+ private fun buildVerifyResult(credential: LineCredential): WritableMap =
+ Arguments.makeNativeMap(
+ mapOf(
+ "clientId" to channelId,
+ "expiresIn" to credential.accessToken.expiresInMillis / 1000L,
+ "scope" to Scope.join(credential.scopes),
+ )
)
- }
}
diff --git a/android/src/main/java/com/xmartlabs/line/LineLoginPackage.kt b/android/src/main/java/com/xmartlabs/line/LineLoginPackage.kt
index 2c8025f..fcc8c2b 100755
--- a/android/src/main/java/com/xmartlabs/line/LineLoginPackage.kt
+++ b/android/src/main/java/com/xmartlabs/line/LineLoginPackage.kt
@@ -21,7 +21,7 @@ class LineLoginPackage : TurboReactPackage() {
LineLoginModule.NAME,
canOverrideExistingModule = false,
needsEagerInit = false,
- hasConstants = true,
+ hasConstants = false,
isCxxModule = false,
isTurboModule = true
)
diff --git a/src/NativeLineLogin.ts b/src/NativeLineLogin.ts
index 473e445..1ba68cb 100644
--- a/src/NativeLineLogin.ts
+++ b/src/NativeLineLogin.ts
@@ -1,126 +1,156 @@
+/**
+ * Native TurboModule specification for LINE Login.
+ *
+ * This file is consumed by React Native code generation — do not change method
+ * signatures without regenerating the native specs.
+ *
+ * @see https://developers.line.biz/en/docs/line-login/
+ */
import { TurboModule, TurboModuleRegistry } from 'react-native'
+// ─── Enums ───────────────────────────────────────────────────────────────────
+
+/** Controls whether the user is prompted to add the LINE Official Account as a friend during login. */
export enum BotPrompt {
- /** Opens a new screen to add a LINE Official Account as a friend after the user agrees to the permissions in the consent screen. */
+ /** Shows a standalone screen after the consent screen asking the user to add the bot. */
Aggressive = 'aggressive',
- /** Includes an option to add a LINE Official Account as a friend in the consent screen. */
+ /** Adds an inline "Add friend" option inside the consent screen. */
Normal = 'normal',
}
-export enum LoginPermission {
+/** OAuth scopes that can be requested during login. */
+export enum Scope {
+ /** Grants access to the user's email address. Requires channel approval from LINE. */
Email = 'email',
- /** To get an ID token in the login response. */
+ /** Issues an OpenID Connect ID token in the login response. Required to receive `idToken` and `idTokenNonce`. */
OpenId = 'openid',
- /** To get user's profile including the user ID, display name, and the profile image URL in the login response. */
+ /** Grants access to the user's basic profile: display name, picture URL, status message, and user ID. */
Profile = 'profile',
}
-export interface AccessToken {
- /** The value of the access token. */
- accessToken: string
- /** The amount of time until the access token expires. */
- expiresIn: string
- /** The raw string of the ID token bound to the access token. Only if the access token is obtained with the `.openID` permission. */
- idToken?: string
-}
-
-export interface FriendshipStatus {
- /** Whether the LINE Official Account is a friend of the user or not. */
- friendFlag: boolean
-}
+// ─── Parameter types ─────────────────────────────────────────────────────────
export interface SetupParams {
- /** The channel ID of the LINE Login channel. */
- channelId: string
- /** The universal link URL for LINE Login. */
- universalLinkUrl?: string
+ /** The LINE Login channel ID. */
+ readonly channelId: string
+ /**
+ * Universal link URL registered for your LINE Login channel.
+ * @platform ios
+ */
+ readonly universalLinkUrl?: string
}
export interface LoginParams {
- botPrompt?: BotPrompt
- onlyWebLogin?: boolean
- scopes?: LoginPermission[]
+ /**
+ * Whether to skip the LINE app and go straight to the web-based login flow.
+ * @default false
+ */
+ readonly onlyWebLogin?: boolean
+ /**
+ * OAuth scopes to request. Defaults to `[Scope.Profile]` when omitted.
+ * @default [Scope.Profile]
+ */
+ readonly scopes?: Scope[]
+ /**
+ * Controls the bot friend-add prompt shown during login.
+ * Only applicable when a LINE Official Account is linked to your channel.
+ * @default BotPrompt.Normal
+ */
+ readonly botPrompt?: BotPrompt
}
-export interface UserProfile {
- /** User's display name. */
- displayName: string
- /** User's profile image URL. */
- pictureUrl?: string
- /** User's status message. */
- statusMessage?: string
- /** User's user ID. */
- userId: string
+// ─── Response types ──────────────────────────────────────────────────────────
+
+export interface AccessToken {
+ /** Bearer token used to authorize LINE API calls. */
+ readonly accessToken: string
+ /**
+ * Seconds until the access token expires (OAuth standard `expires_in`).
+ * Schedule a proactive refresh before this value reaches zero.
+ */
+ readonly expiresIn: number
+ /**
+ * Raw OpenID Connect ID token string.
+ * Present only when `Scope.OpenId` was requested.
+ */
+ readonly idToken?: string
}
-export interface LoginResult {
- /** The access token obtained by the login process. */
- accessToken: AccessToken
- /** Indicates that the friendship status between the user and the bot changed during the login. This value is
- non-`null` only if the `.botPromptNormal` or `.botPromptAggressive` are specified as part of the
- `LoginManagerOption` object when the user logs in. For more information, see Linking a bot with your LINE
- Login channel at https://developers.line.me/en/docs/line-login/web/link-a-bot/. */
- friendshipStatusChanged?: boolean
- /** The `nonce` value when requesting ID Token during login process. Use this value as a parameter when you
- verify the ID Token against the LINE server. This value is `null` if `.openID` permission is not requested. */
- idTokenNonce?: string
- /** The permissions bound to the `accessToken` object by the authorization process. Scope has them separated by spaces. */
- scope: string
- /** Contains the user profile including the user ID, display name, and so on.
- The value exists only when the `.profile` permission is set in the authorization request. */
- userProfile?: UserProfile
+export interface UserProfile {
+ /** The user's LINE display name. */
+ readonly displayName: string
+ /** URL of the user's profile picture. `undefined` if the user has not set one. */
+ readonly pictureUrl?: string
+ /** The user's LINE status message. `undefined` if the user has not set one. */
+ readonly statusMessage?: string
+ /** The user's LINE user ID. Stable across logins for the same channel. */
+ readonly userId: string
}
-export interface VerifyResult {
- /** The channel ID bound to the access token. */
- clientId: string
- /** The amount of time until the access token expires. */
- expiresIn: string
- /** Valid permissions of the access token separated by spaces */
- scope: string
+export interface FriendshipStatus {
+ /** `true` if the user has added the linked LINE Official Account as a friend. */
+ readonly friendFlag: boolean
}
-export interface Spec extends TurboModule {
+export interface LoginResult {
+ /** The access token issued for this login session. */
+ readonly accessToken: AccessToken
/**
- * Gets the access token of the current user.
- * @returns
+ * `true` if the user's friendship status with the linked LINE Official Account
+ * changed during this login (e.g. they added the bot as a friend).
+ * Only present when `BotPrompt.Normal` or `BotPrompt.Aggressive` was used.
*/
- getCurrentAccessToken(): Promise
+ readonly friendshipStatusChanged?: boolean
/**
- * Gets the friendship status between the LINE Official Account (which is linked to the current channel) and the user.
- * @returns
+ * The nonce tied to the ID token. Pass this to your server when verifying the
+ * ID token via the LINE server. `undefined` unless `Scope.OpenId`
+ * was requested.
+ * @see https://developers.line.biz/en/docs/line-login/verify-id-token/
*/
- getFriendshipStatus(): Promise
+ readonly idTokenNonce?: string
/**
- * Gets the current user profile information.
- * @returns
+ * Space-separated list of OAuth scopes granted (e.g. `"profile openid email"`).
*/
- getProfile(): Promise
+ readonly scope: string
/**
- * Logs in the user.
- * @param params
- * @returns
+ * The user's LINE profile. Present only when `Scope.Profile` was
+ * included in the requested scopes.
*/
+ readonly userProfile?: UserProfile
+}
+
+export interface VerifyResult {
+ /** The LINE Login channel ID the access token was issued for. */
+ readonly clientId: string
+ /** Seconds until the access token expires. */
+ readonly expiresIn: number
+ /** Space-separated list of scopes granted to this access token. */
+ readonly scope: string
+}
+
+// ─── TurboModule spec ────────────────────────────────────────────────────────
+
+/**
+ * Bridge interface consumed by the React Native native module system.
+ * Use the default export for all SDK calls rather than referencing `Spec` directly.
+ */
+export interface Spec extends TurboModule {
+ /** Initializes the LINE SDK with your channel credentials. Must be called before any other method. */
+ setup(params: SetupParams): Promise
+ /** Starts the LINE login flow and resolves with the authenticated session. */
login(params: LoginParams): Promise
- /**
- * Revokes the access token of the current user.
- */
+ /** Revokes the current user's access token and clears the local session. */
logout(): Promise
- /**
- * Refreshes the access token of the current user.
- * @returns
- */
+ /** Returns the locally cached access token without a network call. */
+ getCurrentAccessToken(): Promise
+ /** Exchanges the current access token for a fresh one before it expires. */
refreshAccessToken(): Promise
- /**
- * Initializes the Line SDK.
- * @param params
- */
- setup(params: SetupParams): Promise
- /**
- * Checks whether the access token of the current user is valid.
- * @returns
- */
+ /** Validates the current access token against the LINE server and returns its metadata. */
verifyAccessToken(): Promise
+ /** Fetches the current user's LINE profile. Requires `Scope.Profile`. */
+ getProfile(): Promise
+ /** Returns the friendship status between the current user and the channel's linked LINE Official Account. */
+ getFriendshipStatus(): Promise
}
export default TurboModuleRegistry.getEnforcing('LineLogin')