@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.
- package/LICENSE +21 -0
- package/README.md +106 -2
- package/android/build.gradle +152 -0
- package/android/src/main/AndroidManifest.xml +4 -0
- package/android/src/main/java/expo/modules/clerk/ClerkAuthViewModule.kt +241 -0
- package/android/src/main/java/expo/modules/clerk/ClerkComposeNativeViewHost.kt +148 -0
- package/android/src/main/java/expo/modules/clerk/ClerkExpoModule.kt +1077 -0
- package/android/src/main/java/expo/modules/clerk/ClerkUserButtonViewModule.kt +156 -0
- package/android/src/main/java/expo/modules/clerk/ClerkUserProfileCustomPageState.kt +120 -0
- package/android/src/main/java/expo/modules/clerk/ClerkUserProfileViewModule.kt +315 -0
- package/android/src/test/java/expo/modules/clerk/BiometricCredentialBridgeTest.kt +326 -0
- package/android/src/test/java/expo/modules/clerk/ClerkUserProfileCustomPageStateTest.kt +163 -0
- package/app.plugin.d.ts +5 -0
- package/app.plugin.js +333 -0
- package/dist/AuthView.d.ts +35 -0
- package/dist/AuthView.d.ts.map +1 -0
- package/dist/AuthView.js +79 -0
- package/dist/AuthView.js.map +1 -0
- package/dist/AuthView.types.d.ts +62 -0
- package/dist/AuthView.types.d.ts.map +1 -0
- package/dist/AuthView.types.js +0 -0
- package/dist/EmbeddedNavigation.types.d.ts +24 -0
- package/dist/EmbeddedNavigation.types.d.ts.map +1 -0
- package/dist/EmbeddedNavigation.types.js +0 -0
- package/dist/UserButton.d.ts +38 -0
- package/dist/UserButton.d.ts.map +1 -0
- package/dist/UserButton.js +68 -0
- package/dist/UserButton.js.map +1 -0
- package/dist/UserProfileCustomPages.d.ts +81 -0
- package/dist/UserProfileCustomPages.d.ts.map +1 -0
- package/dist/UserProfileCustomPages.js +140 -0
- package/dist/UserProfileCustomPages.js.map +1 -0
- package/dist/UserProfileView.d.ts +76 -0
- package/dist/UserProfileView.d.ts.map +1 -0
- package/dist/UserProfileView.js +101 -0
- package/dist/UserProfileView.js.map +1 -0
- package/dist/_virtual/_rolldown/runtime.js +29 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +37 -0
- package/dist/specs/NativeClerkAuthView.android.d.ts +15 -0
- package/dist/specs/NativeClerkAuthView.android.d.ts.map +1 -0
- package/dist/specs/NativeClerkAuthView.android.js +8 -0
- package/dist/specs/NativeClerkAuthView.android.js.map +1 -0
- package/dist/specs/NativeClerkAuthView.d.ts +15 -0
- package/dist/specs/NativeClerkAuthView.d.ts.map +1 -0
- package/dist/specs/NativeClerkAuthView.js +9 -0
- package/dist/specs/NativeClerkAuthView.js.map +1 -0
- package/dist/specs/NativeClerkModule.d.ts +4 -0
- package/dist/specs/NativeClerkModule.d.ts.map +1 -0
- package/dist/specs/NativeClerkModule.js +8 -0
- package/dist/specs/NativeClerkModule.js.map +1 -0
- package/dist/specs/NativeClerkModule.types.d.ts +11 -0
- package/dist/specs/NativeClerkModule.types.d.ts.map +1 -0
- package/dist/specs/NativeClerkModule.types.js +0 -0
- package/dist/specs/NativeClerkModule.web.d.ts +4 -0
- package/dist/specs/NativeClerkModule.web.d.ts.map +1 -0
- package/dist/specs/NativeClerkModule.web.js +7 -0
- package/dist/specs/NativeClerkModule.web.js.map +1 -0
- package/dist/specs/NativeClerkUserButtonView.android.d.ts +6 -0
- package/dist/specs/NativeClerkUserButtonView.android.d.ts.map +1 -0
- package/dist/specs/NativeClerkUserButtonView.android.js +8 -0
- package/dist/specs/NativeClerkUserButtonView.android.js.map +1 -0
- package/dist/specs/NativeClerkUserButtonView.d.ts +6 -0
- package/dist/specs/NativeClerkUserButtonView.d.ts.map +1 -0
- package/dist/specs/NativeClerkUserButtonView.js +9 -0
- package/dist/specs/NativeClerkUserButtonView.js.map +1 -0
- package/dist/specs/NativeClerkUserProfileView.android.d.ts +13 -0
- package/dist/specs/NativeClerkUserProfileView.android.d.ts.map +1 -0
- package/dist/specs/NativeClerkUserProfileView.android.js +8 -0
- package/dist/specs/NativeClerkUserProfileView.android.js.map +1 -0
- package/dist/specs/NativeClerkUserProfileView.d.ts +13 -0
- package/dist/specs/NativeClerkUserProfileView.d.ts.map +1 -0
- package/dist/specs/NativeClerkUserProfileView.js +9 -0
- package/dist/specs/NativeClerkUserProfileView.js.map +1 -0
- package/dist/useAuthViewState.d.ts +12 -0
- package/dist/useAuthViewState.d.ts.map +1 -0
- package/dist/useAuthViewState.js +76 -0
- package/dist/useAuthViewState.js.map +1 -0
- package/dist/utils/native-module.d.ts +3 -0
- package/dist/utils/native-module.d.ts.map +1 -0
- package/dist/utils/native-module.js +14 -0
- package/dist/utils/native-module.js.map +1 -0
- package/expo-module.config.json +15 -0
- package/ios/ClerkAppDelegateSubscriber.swift +16 -0
- package/ios/ClerkAuthNativeView.swift +157 -0
- package/ios/ClerkExpo.podspec +67 -0
- package/ios/ClerkExpoModule.swift +315 -0
- package/ios/ClerkNativeBridge.swift +1575 -0
- package/ios/ClerkNativeViewHost.swift +204 -0
- package/ios/ClerkUserButtonNativeView.swift +34 -0
- package/ios/ClerkUserProfileNativeView.swift +92 -0
- package/ios/Tests/ClerkAuthNativeViewPaperTests.swift +30 -0
- package/ios/Tests/ClerkNativeBridgeTests.swift +185 -0
- package/ios/Tests/ClerkNativeViewHostTests.swift +66 -0
- package/ios/Tests/ClerkUserProfileCustomPageStateTests.swift +311 -0
- package/package.json +79 -3
- package/react-native.config.js +8 -0
- package/src/specs/NativeClerkAuthView.android.ts +15 -0
- package/src/specs/NativeClerkAuthView.ts +19 -0
- package/src/specs/NativeClerkModule.ts +6 -0
- package/src/specs/NativeClerkModule.types.ts +9 -0
- package/src/specs/NativeClerkModule.web.ts +3 -0
- package/src/specs/NativeClerkUserButtonView.android.ts +9 -0
- package/src/specs/NativeClerkUserButtonView.ts +11 -0
- package/src/specs/NativeClerkUserProfileView.android.ts +13 -0
- package/src/specs/NativeClerkUserProfileView.ts +17 -0
- 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
|
-
|
|
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
|
-
|
|
12
|
+
<div align="center">
|
|
13
|
+
|
|
14
|
+
[](https://clerk.com/discord)
|
|
15
|
+
[](https://clerk.com/docs?utm_source=github&utm_medium=clerk_expo_native_components)
|
|
16
|
+
[](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,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
|
+
}
|