From fa22bbee5deacd3cbc589d967a6a3484040b0d0b Mon Sep 17 00:00:00 2001 From: ChrisCanin Date: Mon, 18 May 2026 12:55:33 -0700 Subject: [PATCH] feat(expo): Add OrganizationSwitcher native component Bridges the prebuilt OrganizationSwitcher view from clerk-ios and clerk-android into @clerk/expo/native as a Fabric-codegen-spec'd component. Mounting renders the native SwiftUI / Jetpack Compose switcher inline; tapping presents the platform's native overview / account-list / manage sheets, including the full OrganizationProfileView flow (members, domains, invitations, action confirmations) on platforms where it has shipped. Bridge surface: - src/specs/NativeClerkOrganizationSwitcher.ts (Fabric spec) - src/native/OrganizationSwitcher.tsx (RN wrapper, onOrganizationChanged) - ios/ClerkOrganizationSwitcherViewManager.{swift,m} + native view class - android/.../ClerkOrganizationSwitcherViewManager.kt + ExpoView - Updated ClerkExpo.podspec source_files, ClerkPackage createViewManagers, and the ClerkViewFactory protocol with createOrganizationSwitcherView() Depends on upstream native releases not yet shipped: - clerk-ios: PR #411 (sean/mobile-487-ios-prebuilt-org-views) must merge - clerk-android: 1.0.17 must be tagged (#628-#638 merged to main, no release) --- .changeset/expo-organization-switcher.md | 21 +++ packages/expo/android/build.gradle | 10 +- .../ClerkOrganizationSwitcherExpoView.kt | 148 ++++++++++++++++++ .../ClerkOrganizationSwitcherViewManager.kt | 32 ++++ .../java/expo/modules/clerk/ClerkPackage.kt | 1 + packages/expo/ios/ClerkExpo.podspec | 3 +- packages/expo/ios/ClerkExpoModule.swift | 83 ++++++++++ .../ClerkOrganizationSwitcherViewManager.m | 8 + ...ClerkOrganizationSwitcherViewManager.swift | 13 ++ packages/expo/ios/ClerkViewFactory.swift | 49 ++++++ .../expo/src/native/OrganizationSwitcher.tsx | 118 ++++++++++++++ packages/expo/src/native/index.ts | 3 + .../specs/NativeClerkOrganizationSwitcher.ts | 15 ++ 13 files changed, 501 insertions(+), 3 deletions(-) create mode 100644 .changeset/expo-organization-switcher.md create mode 100644 packages/expo/android/src/main/java/expo/modules/clerk/ClerkOrganizationSwitcherExpoView.kt create mode 100644 packages/expo/android/src/main/java/expo/modules/clerk/ClerkOrganizationSwitcherViewManager.kt create mode 100644 packages/expo/ios/ClerkOrganizationSwitcherViewManager.m create mode 100644 packages/expo/ios/ClerkOrganizationSwitcherViewManager.swift create mode 100644 packages/expo/src/native/OrganizationSwitcher.tsx create mode 100644 packages/expo/src/specs/NativeClerkOrganizationSwitcher.ts diff --git a/.changeset/expo-organization-switcher.md b/.changeset/expo-organization-switcher.md new file mode 100644 index 00000000000..a2289cd8432 --- /dev/null +++ b/.changeset/expo-organization-switcher.md @@ -0,0 +1,21 @@ +--- +'@clerk/expo': minor +--- + +Add `` native component to `@clerk/expo/native`. + +Renders a fully native organization switcher inline in your React Native view hierarchy — SwiftUI on iOS and Jetpack Compose on Android — backed by the prebuilt `OrganizationSwitcher` views in `clerk-ios` and `clerk-android`. Tapping the switcher opens the platform's native overview / account-list / manage sheets, including the full `OrganizationProfileView` flow (members, domains, invitations, action confirmations) on platforms where it has shipped. + +```tsx +import { OrganizationSwitcher } from '@clerk/expo/native' + + { + // re-query useOrganization() or refetch as needed + }} +/> +``` + +Active-organization changes are surfaced via the `onOrganizationChanged` callback so consumers can react in JS. + +Requires the underlying native SDKs to include their `OrganizationSwitcher` prebuilt views — `clerk-ios` ≥ the release containing [clerk/clerk-ios#411](https://github.com/clerk/clerk-ios/pull/411) and `clerk-android` ≥ 1.0.17. diff --git a/packages/expo/android/build.gradle b/packages/expo/android/build.gradle index d860cd02919..2452939d3db 100644 --- a/packages/expo/android/build.gradle +++ b/packages/expo/android/build.gradle @@ -18,8 +18,14 @@ ext { credentialsVersion = "1.3.0" googleIdVersion = "1.1.1" kotlinxCoroutinesVersion = "1.7.3" - clerkAndroidApiVersion = "1.0.16" - clerkAndroidUiVersion = "1.0.16" + // OrganizationSwitcher + OrganizationProfileView surface depends on the + // clerk-android org suite merged 2026-05-13..16 (PRs #628–#638). v1.0.16 + // (2026-05-11) predates that work — bump to v1.0.17 once tagged. For + // local testing against unreleased main, publish a snapshot via + // `./gradlew publishToMavenLocal` from a clerk-android worktree and pin + // these to your snapshot version. + clerkAndroidApiVersion = "1.0.17" + clerkAndroidUiVersion = "1.0.17" composeVersion = "1.7.0" activityComposeVersion = "1.9.0" lifecycleVersion = "2.8.0" diff --git a/packages/expo/android/src/main/java/expo/modules/clerk/ClerkOrganizationSwitcherExpoView.kt b/packages/expo/android/src/main/java/expo/modules/clerk/ClerkOrganizationSwitcherExpoView.kt new file mode 100644 index 00000000000..b10e24ab52e --- /dev/null +++ b/packages/expo/android/src/main/java/expo/modules/clerk/ClerkOrganizationSwitcherExpoView.kt @@ -0,0 +1,148 @@ +package expo.modules.clerk + +import android.content.Context +import android.content.res.Configuration +import android.util.Log +import android.widget.FrameLayout +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.Recomposer +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.AndroidUiDispatcher +import androidx.compose.ui.platform.ComposeView +import androidx.lifecycle.compose.LocalLifecycleOwner +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.lifecycle.setViewTreeLifecycleOwner +import androidx.lifecycle.setViewTreeViewModelStoreOwner +import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner +import androidx.savedstate.compose.LocalSavedStateRegistryOwner +import androidx.savedstate.setViewTreeSavedStateRegistryOwner +import com.clerk.api.Clerk +import com.clerk.ui.organizationswitcher.OrganizationSwitcher +import com.facebook.react.bridge.Arguments +import com.facebook.react.bridge.ReactContext +import com.facebook.react.uimanager.events.RCTEventEmitter +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +private const val TAG = "ClerkOrgSwitcherView" + +class ClerkOrganizationSwitcherNativeView(context: Context) : FrameLayout(context) { + var hidePersonal: Boolean = false + + private val activity = ClerkAuthNativeView.findActivity(context) + + private var recomposer: Recomposer? = null + private var recomposerJob: kotlinx.coroutines.Job? = null + + private val composeView = ComposeView(context).also { view -> + activity?.let { act -> + view.setViewTreeLifecycleOwner(act) + view.setViewTreeViewModelStoreOwner(act) + view.setViewTreeSavedStateRegistryOwner(act) + + val recomposerContext = AndroidUiDispatcher.Main + val newRecomposer = Recomposer(recomposerContext) + recomposer = newRecomposer + view.setParentCompositionContext(newRecomposer) + val scope = CoroutineScope(recomposerContext + kotlinx.coroutines.SupervisorJob()) + recomposerJob = scope.coroutineContext[kotlinx.coroutines.Job] + scope.launch { + newRecomposer.runRecomposeAndApplyChanges() + } + } + addView(view, LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)) + } + + override fun onDetachedFromWindow() { + recomposer?.cancel() + recomposerJob?.cancel() + super.onDetachedFromWindow() + } + + fun setupView() { + Log.d(TAG, "setupView - hidePersonal: $hidePersonal") + + composeView.setContent { + // Track the last-known active organization id so we only emit the JS event + // when it actually changes. The native composable's onOrganizationChanged + // callback is a Unit lambda; we read the new id off the session flow. + val session by Clerk.sessionFlow.collectAsStateWithLifecycle() + var lastOrgId by remember { mutableStateOf(Clerk.session?.lastActiveOrganizationId) } + + LaunchedEffect(session?.lastActiveOrganizationId) { + val currentId = session?.lastActiveOrganizationId + if (currentId != lastOrgId) { + lastOrgId = currentId + sendEvent("organizationChanged", mapOf("organizationId" to currentId)) + } + } + + val content = @androidx.compose.runtime.Composable { + // React Native apps don't follow Android system dark mode by default, + // so the surrounding RN UI renders light even when the OS is in + // dark mode. ClerkMaterialTheme's color picker reads + // isSystemInDarkTheme() — pin LocalConfiguration's night flag off + // so it matches what the host app actually displays. + // TODO: surface a `colorScheme` prop so consumers can opt into + // following the host app's Appearance API state instead. + val baseConfig = LocalConfiguration.current + val lightConfig = remember(baseConfig) { + Configuration(baseConfig).apply { + uiMode = (uiMode and Configuration.UI_MODE_NIGHT_MASK.inv()) or + Configuration.UI_MODE_NIGHT_NO + } + } + CompositionLocalProvider(LocalConfiguration provides lightConfig) { + OrganizationSwitcher( + modifier = Modifier.fillMaxWidth(), + clerkTheme = Clerk.customTheme, + // Consumers should compose from @clerk/expo/native + // separately when they want one — keeps parity with iOS, which + // doesn't bundle a UserButton in its OrganizationSwitcher. + showUserButton = false, + onOrganizationChanged = { + // The composable signals completion; we read the new id off + // the session flow via the LaunchedEffect above to get an + // actual id payload. + Log.d(TAG, "Org switch completed") + }, + ) + } + } + + if (activity != null) { + CompositionLocalProvider( + LocalViewModelStoreOwner provides activity, + LocalLifecycleOwner provides activity, + LocalSavedStateRegistryOwner provides activity, + ) { + content() + } + } else { + content() + } + } + } + + private fun sendEvent(type: String, data: Map) { + val reactContext = context as? ReactContext ?: return + val eventData = Arguments.createMap().apply { + putString("type", type) + val jsonString = try { + org.json.JSONObject(data).toString() + } catch (e: Exception) { + "{}" + } + putString("data", jsonString) + } + reactContext.getJSModule(RCTEventEmitter::class.java) + .receiveEvent(id, "onOrganizationEvent", eventData) + } +} diff --git a/packages/expo/android/src/main/java/expo/modules/clerk/ClerkOrganizationSwitcherViewManager.kt b/packages/expo/android/src/main/java/expo/modules/clerk/ClerkOrganizationSwitcherViewManager.kt new file mode 100644 index 00000000000..7ca80d75210 --- /dev/null +++ b/packages/expo/android/src/main/java/expo/modules/clerk/ClerkOrganizationSwitcherViewManager.kt @@ -0,0 +1,32 @@ +package expo.modules.clerk + +import com.facebook.react.common.MapBuilder +import com.facebook.react.uimanager.SimpleViewManager +import com.facebook.react.uimanager.ThemedReactContext +import com.facebook.react.uimanager.annotations.ReactProp +import com.facebook.react.viewmanagers.ClerkOrganizationSwitcherViewManagerInterface + +class ClerkOrganizationSwitcherViewManager : SimpleViewManager(), + ClerkOrganizationSwitcherViewManagerInterface { + + override fun getName(): String = "ClerkOrganizationSwitcherView" + + override fun createViewInstance(reactContext: ThemedReactContext): ClerkOrganizationSwitcherNativeView { + return ClerkOrganizationSwitcherNativeView(reactContext) + } + + @ReactProp(name = "hidePersonal") + override fun setHidePersonal(view: ClerkOrganizationSwitcherNativeView, hidePersonal: Boolean) { + view.hidePersonal = hidePersonal + view.setupView() + } + + override fun getExportedCustomBubblingEventTypeConstants(): MutableMap? { + return MapBuilder.builder() + .put("onOrganizationEvent", MapBuilder.of( + "phasedRegistrationNames", + MapBuilder.of("bubbled", "onOrganizationEvent") + )) + .build() as MutableMap + } +} diff --git a/packages/expo/android/src/main/java/expo/modules/clerk/ClerkPackage.kt b/packages/expo/android/src/main/java/expo/modules/clerk/ClerkPackage.kt index 9a97309ac5e..10a71a436d1 100644 --- a/packages/expo/android/src/main/java/expo/modules/clerk/ClerkPackage.kt +++ b/packages/expo/android/src/main/java/expo/modules/clerk/ClerkPackage.kt @@ -38,6 +38,7 @@ class ClerkPackage : TurboReactPackage() { return listOf( ClerkAuthViewManager(), ClerkUserProfileViewManager(), + ClerkOrganizationSwitcherViewManager(), ) } } diff --git a/packages/expo/ios/ClerkExpo.podspec b/packages/expo/ios/ClerkExpo.podspec index fbd91f9a91c..8e3d74aec15 100644 --- a/packages/expo/ios/ClerkExpo.podspec +++ b/packages/expo/ios/ClerkExpo.podspec @@ -40,7 +40,8 @@ Pod::Spec.new do |s| # because it uses `import ClerkKit` which is only available via SPM in the app target. s.source_files = "ClerkExpoModule.swift", "ClerkExpoModule.m", "ClerkAuthViewManager.swift", "ClerkAuthViewManager.m", - "ClerkUserProfileViewManager.swift", "ClerkUserProfileViewManager.m" + "ClerkUserProfileViewManager.swift", "ClerkUserProfileViewManager.m", + "ClerkOrganizationSwitcherViewManager.swift", "ClerkOrganizationSwitcherViewManager.m" install_modules_dependencies(s) end diff --git a/packages/expo/ios/ClerkExpoModule.swift b/packages/expo/ios/ClerkExpoModule.swift index efd1e142445..5efd6ffd951 100644 --- a/packages/expo/ios/ClerkExpoModule.swift +++ b/packages/expo/ios/ClerkExpoModule.swift @@ -18,6 +18,7 @@ public protocol ClerkViewFactoryProtocol { // Inline rendering — returns UIViewController to preserve SwiftUI lifecycle func createAuthView(mode: String, dismissable: Bool, onEvent: @escaping (String, [String: Any]) -> Void) -> UIViewController? func createUserProfileView(dismissable: Bool, onEvent: @escaping (String, [String: Any]) -> Void) -> UIViewController? + func createOrganizationSwitcherView(hidePersonal: Bool, onEvent: @escaping (String, [String: Any]) -> Void) -> UIViewController? // SDK operations func configure(publishableKey: String, bearerToken: String?) async throws @@ -461,3 +462,85 @@ public class ClerkUserProfileNativeView: UIView { hostingController?.view.frame = bounds } } + +// MARK: - Inline View: ClerkOrganizationSwitcherNativeView + +public class ClerkOrganizationSwitcherNativeView: UIView { + private var hostingController: UIViewController? + private var currentHidePersonal: Bool = false + private var hasInitialized: Bool = false + + @objc var onOrganizationEvent: RCTBubblingEventBlock? + + @objc var hidePersonal: NSNumber? { + didSet { + currentHidePersonal = hidePersonal?.boolValue ?? false + if hasInitialized { updateView() } + } + } + + override init(frame: CGRect) { + super.init(frame: frame) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override public func didMoveToWindow() { + super.didMoveToWindow() + if window != nil && !hasInitialized { + hasInitialized = true + updateView() + } + } + + private func updateView() { + hostingController?.view.removeFromSuperview() + hostingController?.removeFromParent() + hostingController = nil + + guard let factory = clerkViewFactory else { return } + + guard let returnedController = factory.createOrganizationSwitcherView( + hidePersonal: currentHidePersonal, + onEvent: { [weak self] eventName, data in + let jsonData = (try? JSONSerialization.data(withJSONObject: data)) ?? Data() + let jsonString = String(data: jsonData, encoding: .utf8) ?? "{}" + self?.onOrganizationEvent?(["type": eventName, "data": jsonString]) + } + ) else { return } + + if let parentVC = findViewController() { + parentVC.addChild(returnedController) + returnedController.view.frame = bounds + returnedController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight] + returnedController.view.backgroundColor = .clear + addSubview(returnedController.view) + returnedController.didMove(toParent: parentVC) + hostingController = returnedController + } else { + returnedController.view.frame = bounds + returnedController.view.autoresizingMask = [.flexibleWidth, .flexibleHeight] + returnedController.view.backgroundColor = .clear + addSubview(returnedController.view) + hostingController = returnedController + } + } + + private func findViewController() -> UIViewController? { + var responder: UIResponder? = self + while let nextResponder = responder?.next { + if let vc = nextResponder as? UIViewController { + return vc + } + responder = nextResponder + } + return nil + } + + override public func layoutSubviews() { + super.layoutSubviews() + hostingController?.view.frame = bounds + } +} diff --git a/packages/expo/ios/ClerkOrganizationSwitcherViewManager.m b/packages/expo/ios/ClerkOrganizationSwitcherViewManager.m new file mode 100644 index 00000000000..b572aa7bed4 --- /dev/null +++ b/packages/expo/ios/ClerkOrganizationSwitcherViewManager.m @@ -0,0 +1,8 @@ +#import + +@interface RCT_EXTERN_MODULE(ClerkOrganizationSwitcherViewManager, RCTViewManager) + +RCT_EXPORT_VIEW_PROPERTY(hidePersonal, NSNumber) +RCT_EXPORT_VIEW_PROPERTY(onOrganizationEvent, RCTBubblingEventBlock) + +@end diff --git a/packages/expo/ios/ClerkOrganizationSwitcherViewManager.swift b/packages/expo/ios/ClerkOrganizationSwitcherViewManager.swift new file mode 100644 index 00000000000..c61b7432b0a --- /dev/null +++ b/packages/expo/ios/ClerkOrganizationSwitcherViewManager.swift @@ -0,0 +1,13 @@ +import React + +@objc(ClerkOrganizationSwitcherViewManager) +class ClerkOrganizationSwitcherViewManager: RCTViewManager { + + override static func requiresMainQueueSetup() -> Bool { + return true + } + + override func view() -> UIView! { + return ClerkOrganizationSwitcherNativeView() + } +} diff --git a/packages/expo/ios/ClerkViewFactory.swift b/packages/expo/ios/ClerkViewFactory.swift index 7e1925f41be..407fe6b226e 100644 --- a/packages/expo/ios/ClerkViewFactory.swift +++ b/packages/expo/ios/ClerkViewFactory.swift @@ -209,6 +209,20 @@ public final class ClerkViewFactory: ClerkViewFactoryProtocol { ) } + public func createOrganizationSwitcherView( + hidePersonal: Bool, + onEvent: @escaping (String, [String: Any]) -> Void + ) -> UIViewController? { + makeHostingController( + rootView: ClerkInlineOrganizationSwitcherWrapperView( + hidePersonal: hidePersonal, + lightTheme: lightTheme, + darkTheme: darkTheme, + onEvent: onEvent + ) + ) + } + @MainActor public func getSession() async -> [String: Any]? { guard Self.clerkConfigured, let session = Clerk.shared.session else { @@ -647,6 +661,41 @@ struct ClerkInlineAuthWrapperView: View { } } +// MARK: - Inline Organization Switcher Wrapper (for embedded rendering) + +struct ClerkInlineOrganizationSwitcherWrapperView: View { + let hidePersonal: Bool + let lightTheme: ClerkTheme? + let darkTheme: ClerkTheme? + let onEvent: (String, [String: Any]) -> Void + + @Environment(\.colorScheme) private var colorScheme + + // Observe the active organization id to emit `organizationChanged` events. + // The native OrganizationSwitcher view itself does not expose a callback; + // it mutates Clerk.shared.organization via setActive internally. + @State private var lastOrganizationId: String? = Clerk.shared.organization?.id + + var body: some View { + let view = OrganizationSwitcher(hidePersonal: hidePersonal) + .environment(Clerk.shared) + let theme = colorScheme == .dark ? (darkTheme ?? lightTheme) : lightTheme + let themedView = Group { + if let theme { + view.environment(\.clerkTheme, theme) + } else { + view + } + } + themedView + .onChange(of: Clerk.shared.organization?.id) { _, newId in + guard newId != lastOrganizationId else { return } + lastOrganizationId = newId + onEvent("organizationChanged", ["organizationId": newId as Any]) + } + } +} + // MARK: - Inline Profile View Wrapper (for embedded rendering) struct ClerkInlineProfileWrapperView: View { diff --git a/packages/expo/src/native/OrganizationSwitcher.tsx b/packages/expo/src/native/OrganizationSwitcher.tsx new file mode 100644 index 00000000000..ece48a50748 --- /dev/null +++ b/packages/expo/src/native/OrganizationSwitcher.tsx @@ -0,0 +1,118 @@ +import { useCallback } from 'react'; +import type { StyleProp, ViewStyle } from 'react-native'; +import { StyleSheet, Text, View } from 'react-native'; + +import NativeClerkOrganizationSwitcher from '../specs/NativeClerkOrganizationSwitcher'; +import { isNativeSupported } from '../utils/native-module'; + +export interface OrganizationSwitcherProps { + /** + * Hide the personal account row inside the switcher. + * + * iOS only today. Android's prebuilt switcher does not currently expose a + * personal-account row; this prop is a no-op there. Once clerk-android's + * MOBILE-497 parity work lands, the prop will apply on both platforms. + * + * @default false + */ + hidePersonal?: boolean; + + /** + * Invoked after the active organization changes. + * + * `organizationId` is the new active organization's id, or `null` when the + * user switched to their personal account. + */ + onOrganizationChanged?: (event: { organizationId: string | null }) => void; + + /** + * Style applied to the container view. + */ + style?: StyleProp; +} + +/** + * A pre-built native control for switching the active organization. + * + * `OrganizationSwitcher` renders inline within your React Native view hierarchy, + * powered by: + * - **iOS**: clerk-ios (SwiftUI) — https://github.com/clerk/clerk-ios + * - **Android**: clerk-android (Jetpack Compose) — https://github.com/clerk/clerk-android + * + * The switcher only renders when Organizations are enabled in the Clerk + * environment and a user is signed in. Use `useOrganization()` to react to + * organization changes in JS. + * + * @example + * ```tsx + * import { OrganizationSwitcher } from '@clerk/expo/native'; + * + * export default function Header() { + * return ; + * } + * ``` + * + * @see {@link https://clerk.com/docs/components/organization/organization-switcher} Clerk OrganizationSwitcher Documentation + */ +export function OrganizationSwitcher({ + hidePersonal = false, + onOrganizationChanged, + style, +}: OrganizationSwitcherProps) { + const handleOrganizationEvent = useCallback( + (event: { nativeEvent: { type: string; data: string } }) => { + const { type, data: rawData } = event.nativeEvent; + if (type !== 'organizationChanged') { + return; + } + + let parsed: { organizationId?: string | null } = {}; + try { + parsed = typeof rawData === 'string' ? JSON.parse(rawData) : rawData; + } catch { + // Malformed payload — surface a null change so consumers can refresh. + } + + onOrganizationChanged?.({ organizationId: parsed.organizationId ?? null }); + }, + [onOrganizationChanged], + ); + + if (!isNativeSupported || !NativeClerkOrganizationSwitcher) { + return ( + + + {!isNativeSupported + ? 'Native OrganizationSwitcher is only available on iOS and Android' + : 'Native OrganizationSwitcher requires the @clerk/expo plugin. Add "@clerk/expo" to your app.json plugins array.'} + + + ); + } + + return ( + + ); +} + +const styles = StyleSheet.create({ + // Without an explicit size, the underlying RN view has zero height — the + // SwiftUI / Compose content overflows visually but taps fall through to + // the parent. Consumers can override via the `style` prop. + nativeDefault: { + alignSelf: 'stretch', + minHeight: 56, + }, + container: { + justifyContent: 'center', + alignItems: 'center', + }, + text: { + fontSize: 14, + color: '#666', + }, +}); diff --git a/packages/expo/src/native/index.ts b/packages/expo/src/native/index.ts index 8ccd60b6f2c..09d69b0647a 100644 --- a/packages/expo/src/native/index.ts +++ b/packages/expo/src/native/index.ts @@ -22,6 +22,7 @@ * ## Components * * - {@link AuthView} - Authentication flow (sign-in/sign-up), renders inline + * - {@link OrganizationSwitcher} - Active organization switcher, renders inline * - {@link UserProfileView} - User profile and account management, renders inline * - {@link UserButton} - Avatar button that opens native profile modal * @@ -30,6 +31,8 @@ export { AuthView } from './AuthView'; export type { AuthViewProps, AuthViewMode } from './AuthView.types'; +export { OrganizationSwitcher } from './OrganizationSwitcher'; +export type { OrganizationSwitcherProps } from './OrganizationSwitcher'; export { UserButton } from './UserButton'; export type { UserButtonProps } from './UserButton'; export { UserProfileView } from './UserProfileView'; diff --git a/packages/expo/src/specs/NativeClerkOrganizationSwitcher.ts b/packages/expo/src/specs/NativeClerkOrganizationSwitcher.ts new file mode 100644 index 00000000000..1817e22273b --- /dev/null +++ b/packages/expo/src/specs/NativeClerkOrganizationSwitcher.ts @@ -0,0 +1,15 @@ +/* eslint-disable import/namespace, import/default, import/no-named-as-default, import/no-named-as-default-member, simple-import-sort/imports */ +// These deep imports from react-native internals are required by codegen. +import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent'; +import type { HostComponent, ViewProps } from 'react-native'; +import type { BubblingEventHandler } from 'react-native/Libraries/Types/CodegenTypes'; +/* eslint-enable import/namespace, import/default, import/no-named-as-default, import/no-named-as-default-member, simple-import-sort/imports */ + +type OrganizationEvent = Readonly<{ type: string; data: string }>; + +interface NativeProps extends ViewProps { + hidePersonal?: boolean; + onOrganizationEvent?: BubblingEventHandler; +} + +export default codegenNativeComponent('ClerkOrganizationSwitcherView') as HostComponent;