@clerk/expo-native-components 0.0.0-stage → 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +106 -2
  3. package/android/build.gradle +152 -0
  4. package/android/src/main/AndroidManifest.xml +4 -0
  5. package/android/src/main/java/expo/modules/clerk/ClerkAuthViewModule.kt +241 -0
  6. package/android/src/main/java/expo/modules/clerk/ClerkComposeNativeViewHost.kt +148 -0
  7. package/android/src/main/java/expo/modules/clerk/ClerkExpoModule.kt +1077 -0
  8. package/android/src/main/java/expo/modules/clerk/ClerkUserButtonViewModule.kt +156 -0
  9. package/android/src/main/java/expo/modules/clerk/ClerkUserProfileCustomPageState.kt +120 -0
  10. package/android/src/main/java/expo/modules/clerk/ClerkUserProfileViewModule.kt +315 -0
  11. package/android/src/test/java/expo/modules/clerk/BiometricCredentialBridgeTest.kt +326 -0
  12. package/android/src/test/java/expo/modules/clerk/ClerkUserProfileCustomPageStateTest.kt +163 -0
  13. package/app.plugin.d.ts +5 -0
  14. package/app.plugin.js +333 -0
  15. package/dist/AuthView.d.ts +35 -0
  16. package/dist/AuthView.d.ts.map +1 -0
  17. package/dist/AuthView.js +79 -0
  18. package/dist/AuthView.js.map +1 -0
  19. package/dist/AuthView.types.d.ts +62 -0
  20. package/dist/AuthView.types.d.ts.map +1 -0
  21. package/dist/AuthView.types.js +0 -0
  22. package/dist/EmbeddedNavigation.types.d.ts +24 -0
  23. package/dist/EmbeddedNavigation.types.d.ts.map +1 -0
  24. package/dist/EmbeddedNavigation.types.js +0 -0
  25. package/dist/UserButton.d.ts +38 -0
  26. package/dist/UserButton.d.ts.map +1 -0
  27. package/dist/UserButton.js +68 -0
  28. package/dist/UserButton.js.map +1 -0
  29. package/dist/UserProfileCustomPages.d.ts +81 -0
  30. package/dist/UserProfileCustomPages.d.ts.map +1 -0
  31. package/dist/UserProfileCustomPages.js +140 -0
  32. package/dist/UserProfileCustomPages.js.map +1 -0
  33. package/dist/UserProfileView.d.ts +76 -0
  34. package/dist/UserProfileView.d.ts.map +1 -0
  35. package/dist/UserProfileView.js +101 -0
  36. package/dist/UserProfileView.js.map +1 -0
  37. package/dist/_virtual/_rolldown/runtime.js +29 -0
  38. package/dist/index.d.ts +41 -0
  39. package/dist/index.d.ts.map +1 -0
  40. package/dist/index.js +37 -0
  41. package/dist/specs/NativeClerkAuthView.android.d.ts +15 -0
  42. package/dist/specs/NativeClerkAuthView.android.d.ts.map +1 -0
  43. package/dist/specs/NativeClerkAuthView.android.js +8 -0
  44. package/dist/specs/NativeClerkAuthView.android.js.map +1 -0
  45. package/dist/specs/NativeClerkAuthView.d.ts +15 -0
  46. package/dist/specs/NativeClerkAuthView.d.ts.map +1 -0
  47. package/dist/specs/NativeClerkAuthView.js +9 -0
  48. package/dist/specs/NativeClerkAuthView.js.map +1 -0
  49. package/dist/specs/NativeClerkModule.d.ts +4 -0
  50. package/dist/specs/NativeClerkModule.d.ts.map +1 -0
  51. package/dist/specs/NativeClerkModule.js +8 -0
  52. package/dist/specs/NativeClerkModule.js.map +1 -0
  53. package/dist/specs/NativeClerkModule.types.d.ts +11 -0
  54. package/dist/specs/NativeClerkModule.types.d.ts.map +1 -0
  55. package/dist/specs/NativeClerkModule.types.js +0 -0
  56. package/dist/specs/NativeClerkModule.web.d.ts +4 -0
  57. package/dist/specs/NativeClerkModule.web.d.ts.map +1 -0
  58. package/dist/specs/NativeClerkModule.web.js +7 -0
  59. package/dist/specs/NativeClerkModule.web.js.map +1 -0
  60. package/dist/specs/NativeClerkUserButtonView.android.d.ts +6 -0
  61. package/dist/specs/NativeClerkUserButtonView.android.d.ts.map +1 -0
  62. package/dist/specs/NativeClerkUserButtonView.android.js +8 -0
  63. package/dist/specs/NativeClerkUserButtonView.android.js.map +1 -0
  64. package/dist/specs/NativeClerkUserButtonView.d.ts +6 -0
  65. package/dist/specs/NativeClerkUserButtonView.d.ts.map +1 -0
  66. package/dist/specs/NativeClerkUserButtonView.js +9 -0
  67. package/dist/specs/NativeClerkUserButtonView.js.map +1 -0
  68. package/dist/specs/NativeClerkUserProfileView.android.d.ts +13 -0
  69. package/dist/specs/NativeClerkUserProfileView.android.d.ts.map +1 -0
  70. package/dist/specs/NativeClerkUserProfileView.android.js +8 -0
  71. package/dist/specs/NativeClerkUserProfileView.android.js.map +1 -0
  72. package/dist/specs/NativeClerkUserProfileView.d.ts +13 -0
  73. package/dist/specs/NativeClerkUserProfileView.d.ts.map +1 -0
  74. package/dist/specs/NativeClerkUserProfileView.js +9 -0
  75. package/dist/specs/NativeClerkUserProfileView.js.map +1 -0
  76. package/dist/useAuthViewState.d.ts +12 -0
  77. package/dist/useAuthViewState.d.ts.map +1 -0
  78. package/dist/useAuthViewState.js +76 -0
  79. package/dist/useAuthViewState.js.map +1 -0
  80. package/dist/utils/native-module.d.ts +3 -0
  81. package/dist/utils/native-module.d.ts.map +1 -0
  82. package/dist/utils/native-module.js +14 -0
  83. package/dist/utils/native-module.js.map +1 -0
  84. package/expo-module.config.json +15 -0
  85. package/ios/ClerkAppDelegateSubscriber.swift +16 -0
  86. package/ios/ClerkAuthNativeView.swift +157 -0
  87. package/ios/ClerkExpo.podspec +67 -0
  88. package/ios/ClerkExpoModule.swift +315 -0
  89. package/ios/ClerkNativeBridge.swift +1575 -0
  90. package/ios/ClerkNativeViewHost.swift +204 -0
  91. package/ios/ClerkUserButtonNativeView.swift +34 -0
  92. package/ios/ClerkUserProfileNativeView.swift +92 -0
  93. package/ios/Tests/ClerkAuthNativeViewPaperTests.swift +30 -0
  94. package/ios/Tests/ClerkNativeBridgeTests.swift +185 -0
  95. package/ios/Tests/ClerkNativeViewHostTests.swift +66 -0
  96. package/ios/Tests/ClerkUserProfileCustomPageStateTests.swift +311 -0
  97. package/package.json +79 -3
  98. package/react-native.config.js +8 -0
  99. package/src/specs/NativeClerkAuthView.android.ts +15 -0
  100. package/src/specs/NativeClerkAuthView.ts +19 -0
  101. package/src/specs/NativeClerkModule.ts +6 -0
  102. package/src/specs/NativeClerkModule.types.ts +9 -0
  103. package/src/specs/NativeClerkModule.web.ts +3 -0
  104. package/src/specs/NativeClerkUserButtonView.android.ts +9 -0
  105. package/src/specs/NativeClerkUserButtonView.ts +11 -0
  106. package/src/specs/NativeClerkUserProfileView.android.ts +13 -0
  107. package/src/specs/NativeClerkUserProfileView.ts +17 -0
  108. package/src/specs/__tests__/native-view.web.test.ts +43 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2022 Clerk, Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,107 @@
1
- # Temporary Holding Version
1
+ <p align="center">
2
+ <a href="https://clerk.com?utm_source=github&utm_medium=clerk_expo_native_components" target="_blank" rel="noopener noreferrer">
3
+ <picture>
4
+ <source media="(prefers-color-scheme: dark)" srcset="https://images.clerk.com/static/logo-dark-mode-400x400.png">
5
+ <img src="https://images.clerk.com/static/logo-light-mode-400x400.png" height="64">
6
+ </picture>
7
+ </a>
8
+ <br />
9
+ <h1 align="center">@clerk/expo-native-components</h1>
10
+ </p>
2
11
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
12
+ <div align="center">
13
+
14
+ [![Chat on Discord](https://img.shields.io/discord/856971667393609759.svg?logo=discord)](https://clerk.com/discord)
15
+ [![Clerk documentation](https://img.shields.io/badge/documentation-clerk-green.svg)](https://clerk.com/docs?utm_source=github&utm_medium=clerk_expo_native_components)
16
+ [![Follow on X](https://img.shields.io/twitter/follow/clerk?style=social)](https://x.com/intent/follow?screen_name=clerk)
17
+
18
+ [Changelog](https://github.com/clerk/javascript/blob/main/packages/expo-native-components/CHANGELOG.md)
19
+ ·
20
+ [Report a Bug](https://github.com/clerk/javascript/issues/new?assignees=&labels=needs-triage&projects=&template=BUG_REPORT.yml)
21
+ ·
22
+ [Request a Feature](https://feedback.clerk.com/roadmap)
23
+ ·
24
+ [Get help](https://clerk.com/contact/support?utm_source=github&utm_medium=clerk_expo_native_components)
25
+
26
+ </div>
27
+
28
+ ## Getting Started
29
+
30
+ `@clerk/expo-native-components` adds Clerk's prebuilt native components to an Expo app that uses [`@clerk/expo`](https://github.com/clerk/javascript/tree/main/packages/expo). It contains the native modules built on the Clerk iOS and Android SDKs:
31
+
32
+ - **iOS**: [clerk-ios](https://github.com/clerk/clerk-ios) (SwiftUI)
33
+ - **Android**: [clerk-android](https://github.com/clerk/clerk-android) (Jetpack Compose)
34
+
35
+ Install it only if you use `AuthView`, `UserProfileView`, `UserButton`, or biometric credentials. Apps that only use `@clerk/expo` do not include the Clerk native SDKs.
36
+
37
+ ### Prerequisites
38
+
39
+ - `@clerk/expo`
40
+ - Expo 54 or later, with a [development build](https://docs.expo.dev/develop/development-builds/introduction/) (Expo Go is not supported)
41
+ - iOS 17 or later
42
+
43
+ ### Installation
44
+
45
+ ```sh
46
+ npx expo install @clerk/expo @clerk/expo-native-components
47
+ ```
48
+
49
+ Add the config plugin alongside `@clerk/expo` in your app config, then rebuild your native app:
50
+
51
+ ```json
52
+ {
53
+ "expo": {
54
+ "plugins": ["@clerk/expo", "@clerk/expo-native-components"]
55
+ }
56
+ }
57
+ ```
58
+
59
+ The plugin accepts the following options:
60
+
61
+ - `keychainService`: keychain service identifier to share the session with app extensions.
62
+ - `theme`: path to a JSON file with `colors`, `darkColors`, and `design` keys that styles the native components.
63
+
64
+ ## Usage
65
+
66
+ ```tsx
67
+ import { useAuth } from '@clerk/expo';
68
+ import { AuthView } from '@clerk/expo-native-components';
69
+
70
+ export default function SignInScreen() {
71
+ const { isSignedIn } = useAuth();
72
+
73
+ if (isSignedIn) {
74
+ return null;
75
+ }
76
+
77
+ return <AuthView />;
78
+ }
79
+ ```
80
+
81
+ For further information, guides, and examples visit the [Expo reference documentation](https://clerk.com/docs/references/expo/overview?utm_source=github&utm_medium=clerk_expo_native_components).
82
+
83
+ ## Support
84
+
85
+ For help, visit our [support page](https://clerk.com/contact/support?utm_source=github&utm_medium=clerk_expo_native_components).
86
+
87
+ ## Community
88
+
89
+ Join our [Discord community](https://clerk.com/discord) to connect with other developers.
90
+
91
+ ## Contributing
92
+
93
+ We're open to all community contributions! If you'd like to contribute in any way, please read [our contribution guidelines](https://github.com/clerk/javascript/blob/main/docs/CONTRIBUTING.md) and [code of conduct](https://github.com/clerk/javascript/blob/main/docs/CODE_OF_CONDUCT.md).
94
+
95
+ ## Security
96
+
97
+ `@clerk/expo-native-components` follows good practices of security, but 100% security cannot be assured.
98
+
99
+ `@clerk/expo-native-components` is provided **"as is"** without any **warranty**. Use at your own risk.
100
+
101
+ _For more information and to report security issues, please refer to our [security documentation](https://github.com/clerk/javascript/blob/main/docs/SECURITY.md)._
102
+
103
+ ## License
104
+
105
+ This project is licensed under the **MIT license**.
106
+
107
+ See [LICENSE](https://github.com/clerk/javascript/blob/main/packages/expo-native-components/LICENSE) for more information.
@@ -0,0 +1,152 @@
1
+ import groovy.json.JsonSlurper
2
+
3
+ plugins {
4
+ id 'com.android.library'
5
+ id 'org.jetbrains.kotlin.android'
6
+ id 'org.jetbrains.kotlin.plugin.compose' version '2.1.20'
7
+ }
8
+
9
+ def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
10
+ apply from: expoModulesCorePlugin
11
+ applyKotlinExpoModulesCorePlugin()
12
+
13
+ group = 'com.clerk.expo'
14
+ version = '1.0.0'
15
+
16
+ // Set by the @clerk/expo-native-components config plugin to the installed @clerk/expo version.
17
+ def clerkExpoVersion = rootProject.findProperty("clerkExpo.hostSdkVersion")?.toString() ?:
18
+ new JsonSlurper().parse(new File(projectDir, "../package.json")).version.toString()
19
+
20
+ // Dependency versions - centralized for easier updates
21
+ // See: https://docs.gradle.org/current/userguide/version_catalogs.html for app-level version catalogs
22
+ ext {
23
+ kotlinxCoroutinesVersion = "1.7.3"
24
+ clerkAndroidApiVersion = "1.1.11"
25
+ clerkAndroidUiVersion = "1.1.11"
26
+ okhttpVersion = "5.4.0"
27
+ okhttpUrlConnectionVersion = "5.0.0-alpha.16"
28
+ composeVersion = "1.7.0"
29
+ activityComposeVersion = "1.9.0"
30
+ lifecycleVersion = "2.8.0"
31
+ }
32
+
33
+ def safeExtGet(prop, fallback) {
34
+ rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
35
+ }
36
+
37
+ android {
38
+ namespace "expo.modules.clerk"
39
+
40
+ compileSdk safeExtGet("compileSdkVersion", 36)
41
+
42
+ defaultConfig {
43
+ minSdk safeExtGet("minSdkVersion", 24)
44
+ targetSdk safeExtGet("targetSdkVersion", 36)
45
+ versionCode 1
46
+ versionName "1.0.0"
47
+ buildConfigField "String", "CLERK_EXPO_VERSION", "\"${clerkExpoVersion}\""
48
+ }
49
+
50
+ buildTypes {
51
+ release {
52
+ minifyEnabled false
53
+ }
54
+ }
55
+
56
+ compileOptions {
57
+ sourceCompatibility JavaVersion.VERSION_17
58
+ targetCompatibility JavaVersion.VERSION_17
59
+ }
60
+
61
+ kotlinOptions {
62
+ jvmTarget = "17"
63
+ // TODO: Remove once clerk-android pins its transitive deps to a Kotlin version compatible with Expo.
64
+ // clerk-android transitive deps (e.g. kotlinx-serialization-core:1.10.0) ship Kotlin 2.3.x metadata,
65
+ // but Expo SDK 54/55 uses Kotlin 2.1.x. This flag lets the 2.1.x compiler read 2.3.x metadata safely.
66
+ freeCompilerArgs += ['-Xskip-metadata-version-check']
67
+ }
68
+
69
+ buildFeatures {
70
+ buildConfig = true
71
+ compose = true
72
+ }
73
+
74
+ packaging {
75
+ resources {
76
+ excludes += ['META-INF/versions/9/OSGI-INF/MANIFEST.MF']
77
+ }
78
+ }
79
+
80
+ sourceSets {
81
+ main {
82
+ java.srcDirs = ['src/main/java']
83
+ }
84
+ }
85
+ }
86
+
87
+ // clerk-android is compiled with Kotlin 2.3.x, but Expo/RN projects typically use an older
88
+ // Kotlin version (e.g. 2.1.x). This causes "incompatible version of Kotlin" metadata errors
89
+ // at :app:compileDebugKotlin. Applying -Xskip-metadata-version-check allows the older
90
+ // compiler to read 2.3.x metadata without failing.
91
+ try {
92
+ rootProject.allprojects {
93
+ tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach {
94
+ compilerOptions {
95
+ freeCompilerArgs.add('-Xskip-metadata-version-check')
96
+ }
97
+ }
98
+ }
99
+ } catch (Exception ignored) {
100
+ // Future Gradle versions with Isolated Projects may block cross-project configuration.
101
+ // In that case, users should add '@clerk/expo-native-components' to their app.json plugins array instead,
102
+ // which applies the flag via the config plugin's withClerkAndroid.
103
+ }
104
+
105
+ dependencies {
106
+ testImplementation "junit:junit:4.13.2"
107
+
108
+ implementation project(':expo-modules-core')
109
+
110
+ // Coroutines for async operations
111
+ implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlinxCoroutinesVersion"
112
+
113
+ // Clerk Android SDK with prebuilt UI
114
+ // Exclude kotlin-stdlib to prevent 2.3.0 from polluting the project
115
+ // Exclude transitive OkHttp modules so the compatible versions below control resolution.
116
+ implementation("com.clerk:clerk-android-ui:$clerkAndroidUiVersion") {
117
+ exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib'
118
+ exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-jdk7'
119
+ exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-jdk8'
120
+ exclude group: 'com.squareup.okhttp3', module: 'okhttp'
121
+ exclude group: 'com.squareup.okhttp3', module: 'okhttp-urlconnection'
122
+ }
123
+ // clerk-android-telemetry has a transitive dep on the last released
124
+ // clerk-android-api. Pinning the api explicitly here keeps consumers
125
+ // compiling against the same version we ship the UI from.
126
+ implementation("com.clerk:clerk-android-api:$clerkAndroidApiVersion") {
127
+ exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib'
128
+ exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-jdk7'
129
+ exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib-jdk8'
130
+ exclude group: 'com.squareup.okhttp3', module: 'okhttp'
131
+ exclude group: 'com.squareup.okhttp3', module: 'okhttp-urlconnection'
132
+ }
133
+
134
+ implementation('com.squareup.okhttp3:okhttp') {
135
+ version { strictly(okhttpVersion) }
136
+ }
137
+ implementation('com.squareup.okhttp3:logging-interceptor') {
138
+ version { strictly(okhttpVersion) }
139
+ }
140
+ implementation('com.squareup.okhttp3:okhttp-urlconnection') {
141
+ version { strictly(okhttpUrlConnectionVersion) }
142
+ }
143
+
144
+ // Jetpack Compose for wrapping Clerk views
145
+ implementation "androidx.compose.ui:ui:$composeVersion"
146
+ implementation "androidx.compose.material3:material3:1.3.0"
147
+ implementation "androidx.activity:activity-compose:$activityComposeVersion"
148
+ implementation "androidx.lifecycle:lifecycle-runtime-compose:$lifecycleVersion"
149
+ implementation "androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycleVersion"
150
+
151
+ testImplementation "junit:junit:4.13.2"
152
+ }
@@ -0,0 +1,4 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+ <application>
3
+ </application>
4
+ </manifest>
@@ -0,0 +1,241 @@
1
+ @file:OptIn(FrameworkIntegrationApi::class)
2
+
3
+ package expo.modules.clerk
4
+
5
+ import android.content.Context
6
+ import android.util.Log
7
+ import android.view.View
8
+ import android.view.ViewGroup
9
+ import androidx.compose.foundation.layout.fillMaxSize
10
+ import androidx.compose.foundation.layout.size
11
+ import androidx.compose.foundation.layout.wrapContentSize
12
+ import androidx.compose.runtime.Composable
13
+ import androidx.compose.runtime.getValue
14
+ import androidx.compose.runtime.key
15
+ import androidx.compose.ui.Modifier
16
+ import androidx.compose.ui.platform.LocalDensity
17
+ import androidx.compose.ui.unit.dp
18
+ import androidx.compose.ui.viewinterop.AndroidView
19
+ import androidx.lifecycle.ViewModelStore
20
+ import androidx.lifecycle.ViewModelStoreOwner
21
+ import androidx.lifecycle.compose.collectAsStateWithLifecycle
22
+ import com.clerk.api.Clerk
23
+ import com.clerk.api.FrameworkIntegrationApi
24
+ import com.clerk.api.ui.ClerkDesign
25
+ import com.clerk.api.ui.ClerkTheme
26
+ import com.clerk.ui.auth.AuthMode
27
+ import com.clerk.ui.auth.AuthView
28
+ import com.clerk.ui.navigation.ClerkHostBackActionProvider
29
+ import expo.modules.kotlin.AppContext
30
+ import expo.modules.kotlin.modules.Module
31
+ import expo.modules.kotlin.modules.ModuleDefinition
32
+ import expo.modules.kotlin.viewevent.EventDispatcher
33
+
34
+ private const val TAG = "ClerkAuthViewModule"
35
+
36
+ private fun debugLog(tag: String, message: String) {
37
+ if (BuildConfig.DEBUG) {
38
+ Log.d(tag, message)
39
+ }
40
+ }
41
+
42
+ class ClerkAuthNativeView(context: Context, appContext: AppContext) : ClerkComposeNativeViewHost(context, appContext) {
43
+ var isDismissible: Boolean = true
44
+ var logoMaxHeight: Float? = null
45
+ var mode: String? = null
46
+ var logoView: View? = null
47
+ private set
48
+ var hostBackButton: Boolean = false
49
+
50
+ private var logoWidth = 0
51
+ private var logoHeight = 0
52
+ private val logoLayoutListener =
53
+ View.OnLayoutChangeListener { view, left, top, right, bottom, _, _, _, _ ->
54
+ val width = right - left
55
+ val height = bottom - top
56
+ if (view === logoView && (width != logoWidth || height != logoHeight)) {
57
+ logoWidth = width
58
+ logoHeight = height
59
+ setupView()
60
+ }
61
+ }
62
+
63
+ private val onAuthEvent by EventDispatcher()
64
+ private val onHostBack by EventDispatcher()
65
+
66
+ init {
67
+ // At cold start, ClerkExpoModule.configure() may run before React's
68
+ // host-resume sync, so this view's construction is a reliable second hook.
69
+ activity?.let { Clerk.attachActivity(it) }
70
+ }
71
+
72
+ // Per-view ViewModelStoreOwner so the AuthView's ViewModels (including its
73
+ // navigation state) are scoped to THIS view instance, not the activity.
74
+ // Without this, the AuthView's navigation persists across mount/unmount
75
+ // cycles within the same activity, leaving the user stuck on whatever screen
76
+ // (e.g. "Get help") was last navigated to before sign-out.
77
+ private val viewModelStoreOwner = object : ViewModelStoreOwner {
78
+ private val store = ViewModelStore()
79
+ override val viewModelStore: ViewModelStore = store
80
+ }
81
+
82
+ private var dismissalEventSent: Boolean = false
83
+
84
+ override fun localViewModelStoreOwner(): ViewModelStoreOwner = viewModelStoreOwner
85
+
86
+ override fun onHostDetachedFromWindow() {
87
+ // Clear our per-view ViewModelStore so any AuthView ViewModels are GC'd.
88
+ viewModelStoreOwner.viewModelStore.clear()
89
+ // Reset so a detached-then-reattached view can emit dismissed again.
90
+ dismissalEventSent = false
91
+ }
92
+
93
+ @Composable
94
+ override fun Content() {
95
+ debugLog(TAG, "setupView - mode: $mode, isDismissible: $isDismissible, hostBackButton: $hostBackButton, activity: $activity")
96
+
97
+ if (hostBackButton) {
98
+ ClerkHostBackActionProvider(onHostBack = { onHostBack(mapOf()) }) { AuthContent() }
99
+ } else {
100
+ AuthContent()
101
+ }
102
+ }
103
+
104
+ @Composable
105
+ private fun AuthContent() {
106
+ // An AuthView composed before Clerk has loaded the environment renders no
107
+ // form and never recovers, so recreate it once loaded.
108
+ val isInitialized by Clerk.isInitialized.collectAsStateWithLifecycle()
109
+ key(isInitialized) {
110
+ AuthContentBody()
111
+ }
112
+ }
113
+
114
+ @Composable
115
+ private fun AuthContentBody() {
116
+ AuthView(
117
+ modifier = Modifier.fillMaxSize(),
118
+ clerkTheme = authTheme(),
119
+ logo = logoView?.let { view ->
120
+ { ReactLogoView(view = view, width = logoWidth, height = logoHeight) }
121
+ },
122
+ mode = authMode(mode),
123
+ isDismissible = isDismissible,
124
+ onDismiss = ::sendDismissEvent,
125
+ onAuthComplete = {
126
+ sendDismissEvent()
127
+ },
128
+ )
129
+ }
130
+
131
+ fun setLogoView(view: View) {
132
+ if (logoView === view) return
133
+ logoView?.removeOnLayoutChangeListener(logoLayoutListener)
134
+ logoView = view
135
+ logoWidth = view.width
136
+ logoHeight = view.height
137
+ view.addOnLayoutChangeListener(logoLayoutListener)
138
+ setupView()
139
+ }
140
+
141
+ fun removeLogoView(view: View) {
142
+ if (logoView !== view) return
143
+ view.removeOnLayoutChangeListener(logoLayoutListener)
144
+ logoView = null
145
+ logoWidth = 0
146
+ logoHeight = 0
147
+ setupView()
148
+ }
149
+
150
+ private fun authTheme(): ClerkTheme? {
151
+ val maxHeight = logoMaxHeight ?: return Clerk.customTheme
152
+ val theme = Clerk.customTheme ?: ClerkTheme()
153
+ val design = theme.design ?: ClerkDesign()
154
+ return theme.copy(design = design.copy(logoMaxHeight = maxHeight.dp))
155
+ }
156
+
157
+ private fun sendEvent(type: String) {
158
+ onAuthEvent(mapOf("type" to type))
159
+ }
160
+
161
+ private fun sendDismissEvent() {
162
+ if (dismissalEventSent) return
163
+ dismissalEventSent = true
164
+ sendEvent("dismissed")
165
+ }
166
+
167
+ private fun authMode(mode: String?): AuthMode = when (mode) {
168
+ "signIn" -> AuthMode.SignIn
169
+ "signUp" -> AuthMode.SignUp
170
+ else -> AuthMode.SignInOrUp
171
+ }
172
+ }
173
+
174
+ @Composable
175
+ private fun ReactLogoView(view: View, width: Int, height: Int) {
176
+ val density = LocalDensity.current
177
+ val modifier =
178
+ if (width > 0 && height > 0) {
179
+ with(density) { Modifier.size(width.toDp(), height.toDp()) }
180
+ } else {
181
+ Modifier.wrapContentSize()
182
+ }
183
+
184
+ AndroidView(
185
+ modifier = modifier,
186
+ factory = {
187
+ (view.parent as? ViewGroup)?.removeView(view)
188
+ view
189
+ },
190
+ )
191
+ }
192
+
193
+ class ClerkAuthViewModule : Module() {
194
+ override fun definition() = ModuleDefinition {
195
+ Name("ClerkAuthView")
196
+
197
+ View(ClerkAuthNativeView::class) {
198
+ Events("onAuthEvent", "onHostBack")
199
+
200
+ GroupView<ClerkAuthNativeView> {
201
+ AddChildView<View> { parent, child, _ ->
202
+ parent.setLogoView(child)
203
+ }
204
+ GetChildCount { parent ->
205
+ if (parent.logoView == null) 0 else 1
206
+ }
207
+ GetChildViewAt<View> { parent, index ->
208
+ if (index == 0) parent.logoView else null
209
+ }
210
+ RemoveChildView<View> { parent, child ->
211
+ parent.removeLogoView(child)
212
+ }
213
+ RemoveChildViewAt { parent, index ->
214
+ if (index == 0) {
215
+ parent.logoView?.let(parent::removeLogoView)
216
+ }
217
+ }
218
+ }
219
+
220
+ Prop("mode") { view: ClerkAuthNativeView, mode: String? ->
221
+ view.mode = mode
222
+ }
223
+
224
+ Prop("isDismissible") { view: ClerkAuthNativeView, isDismissible: Boolean ->
225
+ view.isDismissible = isDismissible
226
+ }
227
+
228
+ Prop("logoMaxHeight") { view: ClerkAuthNativeView, logoMaxHeight: Float? ->
229
+ view.logoMaxHeight = logoMaxHeight
230
+ }
231
+
232
+ Prop("hostBackButton") { view: ClerkAuthNativeView, hostBackButton: Boolean ->
233
+ view.hostBackButton = hostBackButton
234
+ }
235
+
236
+ OnViewDidUpdateProps { view: ClerkAuthNativeView ->
237
+ view.setupView()
238
+ }
239
+ }
240
+ }
241
+ }
@@ -0,0 +1,148 @@
1
+ package expo.modules.clerk
2
+
3
+ import android.content.Context
4
+ import android.content.ContextWrapper
5
+ import android.view.View
6
+ import androidx.activity.ComponentActivity
7
+ import androidx.compose.runtime.Composable
8
+ import androidx.compose.runtime.CompositionLocalProvider
9
+ import androidx.compose.runtime.Recomposer
10
+ import androidx.compose.ui.platform.AndroidUiDispatcher
11
+ import androidx.compose.ui.platform.ComposeView
12
+ import androidx.compose.ui.platform.ViewCompositionStrategy
13
+ import androidx.lifecycle.ViewModelStoreOwner
14
+ import androidx.lifecycle.compose.LocalLifecycleOwner
15
+ import androidx.lifecycle.setViewTreeLifecycleOwner
16
+ import androidx.lifecycle.setViewTreeViewModelStoreOwner
17
+ import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner
18
+ import androidx.savedstate.compose.LocalSavedStateRegistryOwner
19
+ import androidx.savedstate.setViewTreeSavedStateRegistryOwner
20
+ import expo.modules.kotlin.AppContext
21
+ import expo.modules.kotlin.views.ExpoView
22
+ import kotlinx.coroutines.CoroutineScope
23
+ import kotlinx.coroutines.Job
24
+ import kotlinx.coroutines.launch
25
+
26
+ abstract class ClerkComposeNativeViewHost(
27
+ context: Context,
28
+ appContext: AppContext,
29
+ private val retainCompositionOnDetach: Boolean = false,
30
+ ) : ExpoView(context, appContext) {
31
+ protected val activity: ComponentActivity? = findActivity(context)
32
+
33
+ private var recomposer: Recomposer? = null
34
+ private var recomposerJob: Job? = null
35
+
36
+ private val composeView = ComposeView(context).also { view ->
37
+ if (retainCompositionOnDetach) {
38
+ // Native tab navigators detach inactive screens from the window. Keep the
39
+ // composition alive until React actually destroys this Expo view so Clerk's
40
+ // internal navigation back stack survives a tab switch.
41
+ view.setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
42
+ }
43
+ activity?.let { act ->
44
+ view.setViewTreeLifecycleOwner(act)
45
+ view.setViewTreeViewModelStoreOwner(act)
46
+ view.setViewTreeSavedStateRegistryOwner(act)
47
+ }
48
+ addView(view, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT))
49
+ }
50
+
51
+ init {
52
+ // Fabric measures before window attach; without a parent composition context, ComposeView.onMeasure throws.
53
+ startRecomposer()
54
+ }
55
+
56
+ override fun onAttachedToWindow() {
57
+ super.onAttachedToWindow()
58
+ setupView()
59
+ }
60
+
61
+ override fun onDetachedFromWindow() {
62
+ if (!retainCompositionOnDetach) {
63
+ stopRecomposer()
64
+ }
65
+ onHostDetachedFromWindow()
66
+ super.onDetachedFromWindow()
67
+ }
68
+
69
+ fun destroyHost() {
70
+ composeView.disposeComposition()
71
+ stopRecomposer()
72
+ onHostDestroyed()
73
+ }
74
+
75
+ private fun startRecomposer() {
76
+ if (activity == null || recomposerJob?.isActive == true) return
77
+
78
+ // Navigation can detach and later reattach the same host, so a reattached ComposeView needs a fresh recomposer.
79
+ val recomposerContext = AndroidUiDispatcher.Main
80
+ val newRecomposer = Recomposer(recomposerContext)
81
+ recomposer = newRecomposer
82
+ composeView.setParentCompositionContext(newRecomposer)
83
+ recomposerJob = CoroutineScope(recomposerContext).launch {
84
+ newRecomposer.runRecomposeAndApplyChanges()
85
+ }
86
+ }
87
+
88
+ private fun stopRecomposer() {
89
+ recomposer?.cancel()
90
+ recomposerJob?.cancel()
91
+ recomposer = null
92
+ recomposerJob = null
93
+ }
94
+
95
+ fun setupView() {
96
+ startRecomposer()
97
+ composeView.setContent {
98
+ val viewModelStoreOwner = localViewModelStoreOwner()
99
+
100
+ if (activity != null && viewModelStoreOwner != null) {
101
+ CompositionLocalProvider(
102
+ LocalViewModelStoreOwner provides viewModelStoreOwner,
103
+ LocalLifecycleOwner provides activity,
104
+ LocalSavedStateRegistryOwner provides activity,
105
+ ) {
106
+ Content()
107
+ }
108
+ } else {
109
+ Content()
110
+ }
111
+ }
112
+ }
113
+
114
+ protected open fun localViewModelStoreOwner(): ViewModelStoreOwner? = activity
115
+
116
+ protected open fun onHostDetachedFromWindow() {}
117
+
118
+ protected open fun onHostDestroyed() {}
119
+
120
+ protected fun layoutAndroidViewHandler(view: View) {
121
+ view.post {
122
+ val holder = view.parent as? View ?: return@post
123
+ val handler = holder.parent as? View ?: return@post
124
+ val owner = handler.parent as? View ?: return@post
125
+
126
+ // AndroidView's handler can enter the hierarchy after its Compose owner was laid out.
127
+ handler.measure(
128
+ View.MeasureSpec.makeMeasureSpec(owner.width, View.MeasureSpec.EXACTLY),
129
+ View.MeasureSpec.makeMeasureSpec(owner.height, View.MeasureSpec.EXACTLY),
130
+ )
131
+ handler.layout(0, 0, owner.width, owner.height)
132
+ }
133
+ }
134
+
135
+ @Composable
136
+ protected abstract fun Content()
137
+
138
+ companion object {
139
+ fun findActivity(context: Context): ComponentActivity? {
140
+ var ctx: Context? = context
141
+ while (ctx != null) {
142
+ if (ctx is ComponentActivity) return ctx
143
+ ctx = (ctx as? ContextWrapper)?.baseContext
144
+ }
145
+ return null
146
+ }
147
+ }
148
+ }