@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
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
package expo.modules.clerk
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.view.View
|
|
5
|
+
import android.view.ViewGroup
|
|
6
|
+
import androidx.compose.foundation.layout.Box
|
|
7
|
+
import androidx.compose.foundation.layout.fillMaxSize
|
|
8
|
+
import androidx.compose.runtime.Composable
|
|
9
|
+
import androidx.compose.runtime.DisposableEffect
|
|
10
|
+
import androidx.compose.runtime.LaunchedEffect
|
|
11
|
+
import androidx.compose.runtime.getValue
|
|
12
|
+
import androidx.compose.ui.Alignment
|
|
13
|
+
import androidx.compose.ui.Modifier
|
|
14
|
+
import androidx.compose.ui.viewinterop.AndroidView
|
|
15
|
+
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|
16
|
+
import com.clerk.api.Clerk
|
|
17
|
+
import com.clerk.ui.userprofile.custom.LocalUserProfileCustomNavigator
|
|
18
|
+
import com.clerk.ui.userprofile.custom.UserProfileCustomRow
|
|
19
|
+
import com.clerk.ui.userbutton.UserButton
|
|
20
|
+
import expo.modules.kotlin.AppContext
|
|
21
|
+
import expo.modules.kotlin.modules.Module
|
|
22
|
+
import expo.modules.kotlin.modules.ModuleDefinition
|
|
23
|
+
import expo.modules.kotlin.viewevent.EventDispatcher
|
|
24
|
+
|
|
25
|
+
class ClerkUserButtonNativeView(context: Context, appContext: AppContext) : ClerkComposeNativeViewHost(context, appContext) {
|
|
26
|
+
private var customPagesJson: String = "[]"
|
|
27
|
+
private val customPageViews = mutableListOf<View>()
|
|
28
|
+
private val onCustomPageEvent by EventDispatcher()
|
|
29
|
+
private val customPageState =
|
|
30
|
+
ClerkUserProfileCustomPageState(resetCoveredPathsWhenInactive = true) { type, path ->
|
|
31
|
+
onCustomPageEvent(mapOf("type" to type, "path" to path))
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
init {
|
|
35
|
+
activity?.let { Clerk.attachActivity(it) }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
@Composable
|
|
39
|
+
override fun Content() {
|
|
40
|
+
val user by Clerk.userFlow.collectAsStateWithLifecycle()
|
|
41
|
+
|
|
42
|
+
LaunchedEffect(user?.id) { customPageState.userDidChange(user?.id) }
|
|
43
|
+
|
|
44
|
+
Box(
|
|
45
|
+
modifier = Modifier.fillMaxSize(),
|
|
46
|
+
contentAlignment = Alignment.Center,
|
|
47
|
+
) {
|
|
48
|
+
UserButton(
|
|
49
|
+
clerkTheme = Clerk.customTheme,
|
|
50
|
+
customRows = customRows(),
|
|
51
|
+
customDestination =
|
|
52
|
+
if (customPageViews.isEmpty()) null
|
|
53
|
+
else { routeKey -> CustomPageDestination(routeKey) },
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
fun addCustomPageView(view: View, index: Int) {
|
|
59
|
+
(view.parent as? ViewGroup)?.removeView(view)
|
|
60
|
+
customPageViews.add(index.coerceIn(0, customPageViews.size), view)
|
|
61
|
+
setupView()
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
fun removeCustomPageView(view: View) {
|
|
65
|
+
customPageViews.remove(view)
|
|
66
|
+
(view.parent as? ViewGroup)?.removeView(view)
|
|
67
|
+
setupView()
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
fun customPageViewAt(index: Int): View? = customPageViews.getOrNull(index)
|
|
71
|
+
|
|
72
|
+
fun customPageCount(): Int = customPageViews.size
|
|
73
|
+
|
|
74
|
+
fun setCustomPages(customPages: String) {
|
|
75
|
+
if (customPagesJson == customPages) return
|
|
76
|
+
val validPaths = runCatching { userProfileCustomPagePaths(customPages) }.getOrDefault(emptySet())
|
|
77
|
+
customPageState.reconcileCustomPagePaths(validPaths)
|
|
78
|
+
customPagesJson = customPages
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
fun navigateCustomPage(action: String, routeKey: String?) {
|
|
82
|
+
customPageState.navigate(action, routeKey)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
@Composable
|
|
86
|
+
private fun CustomPageDestination(routeKey: String) {
|
|
87
|
+
val customNavigator = LocalUserProfileCustomNavigator.current
|
|
88
|
+
val pages = customPages()
|
|
89
|
+
val view = customPageViews.getOrNull(pages.indexOfFirst { it.routeKey == routeKey })
|
|
90
|
+
|
|
91
|
+
LaunchedEffect(routeKey, customNavigator, view) {
|
|
92
|
+
customPageState.configureNavigation(
|
|
93
|
+
navigateBack = customNavigator::navigateBack,
|
|
94
|
+
popToRoot = customNavigator::popToRoot,
|
|
95
|
+
push = customNavigator::push,
|
|
96
|
+
)
|
|
97
|
+
if (view == null) {
|
|
98
|
+
customNavigator.popToRoot()
|
|
99
|
+
return@LaunchedEffect
|
|
100
|
+
}
|
|
101
|
+
layoutAndroidViewHandler(view)
|
|
102
|
+
customPageState.pageDidPresent(routeKey)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (view == null) return
|
|
106
|
+
|
|
107
|
+
DisposableEffect(routeKey) {
|
|
108
|
+
onDispose { customPageState.pageDidDismiss(routeKey) }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
AndroidView(
|
|
112
|
+
modifier = Modifier.fillMaxSize(),
|
|
113
|
+
factory = {
|
|
114
|
+
(view.parent as? ViewGroup)?.removeView(view)
|
|
115
|
+
view
|
|
116
|
+
},
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private fun customRows(): List<UserProfileCustomRow> = customPages().filter { it.showAsRow }.map { it.nativeRow }
|
|
121
|
+
|
|
122
|
+
private fun customPages(): List<ClerkUserProfileCustomPageConfig> =
|
|
123
|
+
runCatching { parseUserProfileCustomPages(customPagesJson, customPageViews.size) }.getOrDefault(emptyList())
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
class ClerkUserButtonViewModule : Module() {
|
|
127
|
+
override fun definition() = ModuleDefinition {
|
|
128
|
+
Name("ClerkUserButtonView")
|
|
129
|
+
|
|
130
|
+
View(ClerkUserButtonNativeView::class) {
|
|
131
|
+
Events("onCustomPageEvent")
|
|
132
|
+
|
|
133
|
+
GroupView<ClerkUserButtonNativeView> {
|
|
134
|
+
AddChildView<View> { parent, child, index -> parent.addCustomPageView(child, index) }
|
|
135
|
+
GetChildCount { parent -> parent.customPageCount() }
|
|
136
|
+
GetChildViewAt<View> { parent, index -> parent.customPageViewAt(index) }
|
|
137
|
+
RemoveChildView<View> { parent, child -> parent.removeCustomPageView(child) }
|
|
138
|
+
RemoveChildViewAt { parent, index -> parent.customPageViewAt(index)?.let(parent::removeCustomPageView) }
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
Prop("customPages") { view: ClerkUserButtonNativeView, customPages: String ->
|
|
142
|
+
view.setCustomPages(customPages)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
AsyncFunction("navigateCustomPage") {
|
|
146
|
+
view: ClerkUserButtonNativeView,
|
|
147
|
+
action: String,
|
|
148
|
+
routeKey: String? -> view.navigateCustomPage(action, routeKey)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
OnViewDidUpdateProps { view: ClerkUserButtonNativeView ->
|
|
152
|
+
view.setupView()
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
package expo.modules.clerk
|
|
2
|
+
|
|
3
|
+
import android.os.Handler
|
|
4
|
+
import android.os.Looper
|
|
5
|
+
|
|
6
|
+
private fun postToMainThread(action: () -> Unit) {
|
|
7
|
+
Handler(Looper.getMainLooper()).post { action() }
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
internal class ClerkUserProfileCustomPageState(
|
|
11
|
+
private val resetCoveredPathsWhenInactive: Boolean = false,
|
|
12
|
+
private val postInactiveReset: ((() -> Unit) -> Unit) = ::postToMainThread,
|
|
13
|
+
private val pageEventHandler: (type: String, path: String) -> Unit,
|
|
14
|
+
) {
|
|
15
|
+
private val retainedPaths = mutableListOf<String>()
|
|
16
|
+
private var navigateBackAction: (() -> Unit)? = null
|
|
17
|
+
private var popToRootAction: (() -> Unit)? = null
|
|
18
|
+
private var pushAction: ((String) -> Unit)? = null
|
|
19
|
+
private var hasObservedUserId = false
|
|
20
|
+
private var observedUserId: String? = null
|
|
21
|
+
private var navigationTransitionGeneration = 0
|
|
22
|
+
|
|
23
|
+
fun configureNavigation(
|
|
24
|
+
navigateBack: () -> Unit,
|
|
25
|
+
popToRoot: () -> Unit,
|
|
26
|
+
push: (String) -> Unit,
|
|
27
|
+
) {
|
|
28
|
+
navigateBackAction = navigateBack
|
|
29
|
+
popToRootAction = popToRoot
|
|
30
|
+
pushAction = push
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
fun pageDidPresent(path: String) {
|
|
34
|
+
cancelPendingInactiveReset()
|
|
35
|
+
if (path !in retainedPaths) {
|
|
36
|
+
retainedPaths.add(path)
|
|
37
|
+
}
|
|
38
|
+
pageEventHandler("presented", path)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
fun pageDidDismiss(path: String) {
|
|
42
|
+
val pathIndex = retainedPaths.lastIndexOf(path)
|
|
43
|
+
if (pathIndex == -1) return
|
|
44
|
+
|
|
45
|
+
// Navigation 3 disposes a destination when another page covers it. Keep that
|
|
46
|
+
// earlier path until it is actually removed from the native back stack.
|
|
47
|
+
if (pathIndex == retainedPaths.lastIndex) {
|
|
48
|
+
retainedPaths.removeAt(pathIndex)
|
|
49
|
+
if (resetCoveredPathsWhenInactive) {
|
|
50
|
+
scheduleInactiveReset()
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
pageEventHandler("dismissed", path)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
fun userDidChange(userId: String?) {
|
|
57
|
+
if (!hasObservedUserId) {
|
|
58
|
+
observedUserId = userId
|
|
59
|
+
hasObservedUserId = true
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (observedUserId == userId) return
|
|
64
|
+
observedUserId = userId
|
|
65
|
+
invalidateNavigation()
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
fun reconcileCustomPagePaths(validPaths: Set<String>) {
|
|
69
|
+
if (retainedPaths.all(validPaths::contains)) return
|
|
70
|
+
invalidateNavigation()
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
fun navigate(action: String, routeKey: String?) {
|
|
74
|
+
when (action) {
|
|
75
|
+
"back" -> navigateBackAction?.invoke()
|
|
76
|
+
"popToRoot" -> invalidateNavigation()
|
|
77
|
+
"push" -> {
|
|
78
|
+
val path = routeKey ?: return
|
|
79
|
+
val push = pushAction ?: return
|
|
80
|
+
if (path in retainedPaths) return
|
|
81
|
+
retainedPaths.add(path)
|
|
82
|
+
push(path)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
internal fun retainedPathsForTesting(): List<String> = retainedPaths.toList()
|
|
88
|
+
|
|
89
|
+
private fun invalidateNavigation() {
|
|
90
|
+
cancelPendingInactiveReset()
|
|
91
|
+
if (retainedPaths.isEmpty()) return
|
|
92
|
+
|
|
93
|
+
val dismissedPaths = retainedPaths.asReversed().distinct()
|
|
94
|
+
retainedPaths.clear()
|
|
95
|
+
popToRootAction?.invoke()
|
|
96
|
+
dismissedPaths.forEach { pageEventHandler("dismissed", it) }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private fun scheduleInactiveReset() {
|
|
100
|
+
val generation = ++navigationTransitionGeneration
|
|
101
|
+
postInactiveReset {
|
|
102
|
+
if (generation == navigationTransitionGeneration) {
|
|
103
|
+
resetInactivePaths()
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
private fun cancelPendingInactiveReset() {
|
|
109
|
+
navigationTransitionGeneration += 1
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
private fun resetInactivePaths() {
|
|
113
|
+
navigateBackAction = null
|
|
114
|
+
popToRootAction = null
|
|
115
|
+
pushAction = null
|
|
116
|
+
val dismissedPaths = retainedPaths.asReversed().toList()
|
|
117
|
+
retainedPaths.clear()
|
|
118
|
+
dismissedPaths.forEach { pageEventHandler("dismissed", it) }
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,315 @@
|
|
|
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.runtime.Composable
|
|
11
|
+
import androidx.compose.runtime.DisposableEffect
|
|
12
|
+
import androidx.compose.runtime.LaunchedEffect
|
|
13
|
+
import androidx.compose.runtime.getValue
|
|
14
|
+
import androidx.compose.ui.Modifier
|
|
15
|
+
import androidx.compose.ui.viewinterop.AndroidView
|
|
16
|
+
import androidx.lifecycle.ViewModelStore
|
|
17
|
+
import androidx.lifecycle.ViewModelStoreOwner
|
|
18
|
+
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|
19
|
+
import com.clerk.api.Clerk
|
|
20
|
+
import com.clerk.api.FrameworkIntegrationApi
|
|
21
|
+
import com.clerk.ui.R
|
|
22
|
+
import com.clerk.ui.navigation.ClerkHostBackActionProvider
|
|
23
|
+
import com.clerk.ui.userprofile.UserProfileView
|
|
24
|
+
import com.clerk.ui.userprofile.custom.LocalUserProfileCustomNavigator
|
|
25
|
+
import com.clerk.ui.userprofile.custom.UserProfileCustomRow
|
|
26
|
+
import com.clerk.ui.userprofile.custom.UserProfileCustomRowPlacement
|
|
27
|
+
import com.clerk.ui.userprofile.custom.UserProfileRow
|
|
28
|
+
import com.clerk.ui.userprofile.custom.UserProfileRowIcon
|
|
29
|
+
import com.clerk.ui.userprofile.custom.UserProfileSection
|
|
30
|
+
import expo.modules.kotlin.AppContext
|
|
31
|
+
import expo.modules.kotlin.modules.Module
|
|
32
|
+
import expo.modules.kotlin.modules.ModuleDefinition
|
|
33
|
+
import expo.modules.kotlin.viewevent.EventDispatcher
|
|
34
|
+
import org.json.JSONArray
|
|
35
|
+
import org.json.JSONObject
|
|
36
|
+
|
|
37
|
+
private const val TAG = "ClerkUserProfileViewModule"
|
|
38
|
+
|
|
39
|
+
private fun debugLog(tag: String, message: String) {
|
|
40
|
+
if (BuildConfig.DEBUG) {
|
|
41
|
+
Log.d(tag, message)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
internal data class ClerkUserProfileCustomPageConfig(
|
|
46
|
+
val routeKey: String,
|
|
47
|
+
val title: String,
|
|
48
|
+
val icon: Int,
|
|
49
|
+
val placement: UserProfileCustomRowPlacement,
|
|
50
|
+
val showAsRow: Boolean,
|
|
51
|
+
) {
|
|
52
|
+
val nativeRow: UserProfileCustomRow
|
|
53
|
+
get() =
|
|
54
|
+
UserProfileCustomRow(
|
|
55
|
+
routeKey = routeKey,
|
|
56
|
+
title = title,
|
|
57
|
+
icon = UserProfileRowIcon.Resource(icon),
|
|
58
|
+
placement = placement,
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
internal fun parseUserProfileCustomPages(
|
|
63
|
+
customPagesJson: String,
|
|
64
|
+
customPageCount: Int,
|
|
65
|
+
): List<ClerkUserProfileCustomPageConfig> {
|
|
66
|
+
val pages = JSONArray(customPagesJson)
|
|
67
|
+
return buildList {
|
|
68
|
+
for (index in 0 until minOf(pages.length(), customPageCount)) {
|
|
69
|
+
val page = pages.getJSONObject(index)
|
|
70
|
+
add(
|
|
71
|
+
ClerkUserProfileCustomPageConfig(
|
|
72
|
+
routeKey = page.getString("path"),
|
|
73
|
+
title = page.getString("label"),
|
|
74
|
+
icon = userProfileCustomRowIcon(page.optString("icon")),
|
|
75
|
+
placement = userProfileCustomRowPlacement(page.optJSONObject("placement")),
|
|
76
|
+
showAsRow = page.optBoolean("showAsRow", true),
|
|
77
|
+
),
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
internal fun userProfileCustomPagePaths(customPagesJson: String): Set<String> {
|
|
84
|
+
val pages = JSONArray(customPagesJson)
|
|
85
|
+
return buildSet {
|
|
86
|
+
for (index in 0 until pages.length()) {
|
|
87
|
+
add(pages.getJSONObject(index).getString("path"))
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private fun userProfileCustomRowIcon(icon: String): Int =
|
|
93
|
+
when (icon) {
|
|
94
|
+
"user" -> R.drawable.ic_user
|
|
95
|
+
"profile" -> R.drawable.ic_profile
|
|
96
|
+
"security" -> R.drawable.ic_security
|
|
97
|
+
"billing" -> R.drawable.ic_credit_card
|
|
98
|
+
"key" -> R.drawable.ic_key
|
|
99
|
+
"lock" -> R.drawable.ic_lock
|
|
100
|
+
"email" -> R.drawable.ic_email
|
|
101
|
+
"phone" -> R.drawable.ic_phone
|
|
102
|
+
"add" -> R.drawable.ic_plus
|
|
103
|
+
"switch" -> R.drawable.ic_switch
|
|
104
|
+
"users" -> R.drawable.ic_users
|
|
105
|
+
"warning" -> R.drawable.ic_warning
|
|
106
|
+
"info" -> R.drawable.ic_information_circle
|
|
107
|
+
"globe" -> R.drawable.ic_globe
|
|
108
|
+
"folder" -> R.drawable.ic_folder
|
|
109
|
+
"book" -> R.drawable.ic_folder
|
|
110
|
+
else -> R.drawable.ic_cog
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private fun userProfileCustomRowPlacement(placement: JSONObject?): UserProfileCustomRowPlacement {
|
|
114
|
+
val type = placement?.optString("type")
|
|
115
|
+
return when (type) {
|
|
116
|
+
"sectionStart" -> UserProfileCustomRowPlacement.SectionStart(userProfileSection(placement.optString("section")))
|
|
117
|
+
"before" -> UserProfileCustomRowPlacement.Before(userProfileRow(placement.optString("row")))
|
|
118
|
+
"after" -> UserProfileCustomRowPlacement.After(userProfileRow(placement.optString("row")))
|
|
119
|
+
else -> UserProfileCustomRowPlacement.SectionEnd(userProfileSection(placement?.optString("section")))
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private fun userProfileSection(section: String?): UserProfileSection =
|
|
124
|
+
if (section == "account") UserProfileSection.Account else UserProfileSection.Profile
|
|
125
|
+
|
|
126
|
+
private fun userProfileRow(row: String): UserProfileRow =
|
|
127
|
+
when (row) {
|
|
128
|
+
"security" -> UserProfileRow.Security
|
|
129
|
+
"switchAccount" -> UserProfileRow.SwitchAccount
|
|
130
|
+
"addAccount" -> UserProfileRow.AddAccount
|
|
131
|
+
"signOut" -> UserProfileRow.SignOut
|
|
132
|
+
else -> UserProfileRow.ManageAccount
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
class ClerkUserProfileNativeView(context: Context, appContext: AppContext) :
|
|
136
|
+
ClerkComposeNativeViewHost(context, appContext, retainCompositionOnDetach = true) {
|
|
137
|
+
// clerk-android UserProfileView dismissibility is controlled by its onDismiss callback.
|
|
138
|
+
var isDismissible: Boolean = true
|
|
139
|
+
var hostBackButton: Boolean = false
|
|
140
|
+
private var customPagesJson: String = "[]"
|
|
141
|
+
private val customPageViews = mutableListOf<View>()
|
|
142
|
+
private val onProfileEvent by EventDispatcher()
|
|
143
|
+
private val onCustomPageEvent by EventDispatcher()
|
|
144
|
+
private val onHostBack by EventDispatcher()
|
|
145
|
+
private val customPageState =
|
|
146
|
+
ClerkUserProfileCustomPageState { type, path ->
|
|
147
|
+
onCustomPageEvent(mapOf("type" to type, "path" to path))
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
private val viewModelStoreOwner = object : ViewModelStoreOwner {
|
|
151
|
+
private val store = ViewModelStore()
|
|
152
|
+
override val viewModelStore: ViewModelStore = store
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
override fun localViewModelStoreOwner(): ViewModelStoreOwner = viewModelStoreOwner
|
|
156
|
+
|
|
157
|
+
override fun onHostDestroyed() {
|
|
158
|
+
viewModelStoreOwner.viewModelStore.clear()
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
@Composable
|
|
162
|
+
override fun Content() {
|
|
163
|
+
debugLog(TAG, "setupView - isDismissible: $isDismissible, hostBackButton: $hostBackButton")
|
|
164
|
+
|
|
165
|
+
if (hostBackButton) {
|
|
166
|
+
ClerkHostBackActionProvider(onHostBack = { onHostBack(mapOf()) }) { ProfileView() }
|
|
167
|
+
} else {
|
|
168
|
+
ProfileView()
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
@Composable
|
|
173
|
+
private fun ProfileView() {
|
|
174
|
+
val user by Clerk.userFlow.collectAsStateWithLifecycle()
|
|
175
|
+
|
|
176
|
+
LaunchedEffect(user?.id) { customPageState.userDidChange(user?.id) }
|
|
177
|
+
|
|
178
|
+
UserProfileView(
|
|
179
|
+
clerkTheme = Clerk.customTheme,
|
|
180
|
+
customRows = customRows(),
|
|
181
|
+
customDestination =
|
|
182
|
+
if (customPageViews.isEmpty()) null
|
|
183
|
+
else { routeKey -> CustomPageDestination(routeKey) },
|
|
184
|
+
isDismissible = isDismissible,
|
|
185
|
+
onDismiss = {
|
|
186
|
+
debugLog(TAG, "Profile dismissed")
|
|
187
|
+
sendEvent("dismissed")
|
|
188
|
+
},
|
|
189
|
+
)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
fun addCustomPageView(view: View, index: Int) {
|
|
193
|
+
(view.parent as? ViewGroup)?.removeView(view)
|
|
194
|
+
customPageViews.add(index.coerceIn(0, customPageViews.size), view)
|
|
195
|
+
setupView()
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
fun removeCustomPageView(view: View) {
|
|
199
|
+
customPageViews.remove(view)
|
|
200
|
+
(view.parent as? ViewGroup)?.removeView(view)
|
|
201
|
+
setupView()
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
fun customPageViewAt(index: Int): View? = customPageViews.getOrNull(index)
|
|
205
|
+
|
|
206
|
+
fun customPageCount(): Int = customPageViews.size
|
|
207
|
+
|
|
208
|
+
fun setCustomPages(customPages: String) {
|
|
209
|
+
if (customPagesJson == customPages) return
|
|
210
|
+
val validPaths = runCatching { userProfileCustomPagePaths(customPages) }.getOrDefault(emptySet())
|
|
211
|
+
customPageState.reconcileCustomPagePaths(validPaths)
|
|
212
|
+
customPagesJson = customPages
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
fun navigateCustomPage(action: String, routeKey: String?) {
|
|
216
|
+
customPageState.navigate(action, routeKey)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
@Composable
|
|
220
|
+
private fun CustomPageDestination(routeKey: String) {
|
|
221
|
+
val customNavigator = LocalUserProfileCustomNavigator.current
|
|
222
|
+
val pages = customPages()
|
|
223
|
+
val view = customPageViews.getOrNull(pages.indexOfFirst { it.routeKey == routeKey })
|
|
224
|
+
|
|
225
|
+
LaunchedEffect(routeKey, customNavigator, view) {
|
|
226
|
+
customPageState.configureNavigation(
|
|
227
|
+
navigateBack = customNavigator::navigateBack,
|
|
228
|
+
popToRoot = customNavigator::popToRoot,
|
|
229
|
+
push = customNavigator::push,
|
|
230
|
+
)
|
|
231
|
+
if (view == null) {
|
|
232
|
+
customNavigator.popToRoot()
|
|
233
|
+
return@LaunchedEffect
|
|
234
|
+
}
|
|
235
|
+
layoutAndroidViewHandler(view)
|
|
236
|
+
customPageState.pageDidPresent(routeKey)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (view == null) return
|
|
240
|
+
|
|
241
|
+
DisposableEffect(routeKey) {
|
|
242
|
+
onDispose { customPageState.pageDidDismiss(routeKey) }
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
AndroidView(
|
|
246
|
+
modifier = Modifier.fillMaxSize(),
|
|
247
|
+
factory = {
|
|
248
|
+
(view.parent as? ViewGroup)?.removeView(view)
|
|
249
|
+
view
|
|
250
|
+
},
|
|
251
|
+
)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
private fun customRows(): List<UserProfileCustomRow> {
|
|
255
|
+
return customPages().filter { it.showAsRow }.map { it.nativeRow }
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
private fun customPages(): List<ClerkUserProfileCustomPageConfig> {
|
|
259
|
+
return runCatching {
|
|
260
|
+
parseUserProfileCustomPages(customPagesJson, customPageViews.size)
|
|
261
|
+
}
|
|
262
|
+
.getOrElse {
|
|
263
|
+
debugLog(TAG, "Ignoring invalid custom rows: ${it.message}")
|
|
264
|
+
emptyList()
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
private fun sendEvent(type: String) {
|
|
269
|
+
onProfileEvent(mapOf("type" to type))
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
class ClerkUserProfileViewModule : Module() {
|
|
274
|
+
override fun definition() = ModuleDefinition {
|
|
275
|
+
Name("ClerkUserProfileView")
|
|
276
|
+
|
|
277
|
+
View(ClerkUserProfileNativeView::class) {
|
|
278
|
+
Events("onProfileEvent", "onCustomPageEvent", "onHostBack")
|
|
279
|
+
|
|
280
|
+
GroupView<ClerkUserProfileNativeView> {
|
|
281
|
+
AddChildView<View> { parent, child, index -> parent.addCustomPageView(child, index) }
|
|
282
|
+
GetChildCount { parent -> parent.customPageCount() }
|
|
283
|
+
GetChildViewAt<View> { parent, index -> parent.customPageViewAt(index) }
|
|
284
|
+
RemoveChildView<View> { parent, child -> parent.removeCustomPageView(child) }
|
|
285
|
+
RemoveChildViewAt { parent, index -> parent.customPageViewAt(index)?.let(parent::removeCustomPageView) }
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
Prop("isDismissible") { view: ClerkUserProfileNativeView, isDismissible: Boolean ->
|
|
289
|
+
view.isDismissible = isDismissible
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
Prop("hostBackButton") { view: ClerkUserProfileNativeView, hostBackButton: Boolean ->
|
|
293
|
+
view.hostBackButton = hostBackButton
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
Prop("customPages") { view: ClerkUserProfileNativeView, customPages: String ->
|
|
297
|
+
view.setCustomPages(customPages)
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
AsyncFunction("navigateCustomPage") {
|
|
301
|
+
view: ClerkUserProfileNativeView,
|
|
302
|
+
action: String,
|
|
303
|
+
routeKey: String? -> view.navigateCustomPage(action, routeKey)
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
OnViewDidUpdateProps { view: ClerkUserProfileNativeView ->
|
|
307
|
+
view.setupView()
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
OnViewDestroys { view: ClerkUserProfileNativeView ->
|
|
311
|
+
view.destroyHost()
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|