| author | |
| committer | |
| log | 0f21fcbb6dbabb6d2185a73edf6b865fd64c6019 |
| tree | 1bfaf895e6e5880304c03dc68c74d7a9da74d852 |
| parent | bcc20249cc0a888430f2006d61c11de176d531cd |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>6 files changed, 374 insertions(+), 12 deletions(-)
apps/mobile/global.css+25| ... | @@ -233,6 +233,31 @@ | ... | @@ -233,6 +233,31 @@ |
| 233 | } | 233 | } |
| 234 | } | 234 | } |
| 235 | 235 | ||
| 236 | /* ─── Clerk native profile ──────────────────────────────────────────── */ | ||
| 237 | /* Fixed palette for custom pages inside Clerk's native user profile. Mirrors | ||
| 238 | clerk-theme.json, which themes the SDK's own screens, so ours match them. | ||
| 239 | Kept out of the runtime palette above on purpose: custom themes must not | ||
| 240 | restyle Clerk's chrome. Keep in sync with clerk-theme.json. */ | ||
| 241 | @layer theme { | ||
| 242 | :root { | ||
| 243 | @variant light { | ||
| 244 | --color-clerk-page: #f2f2f7; | ||
| 245 | --color-clerk-foreground: #262626; | ||
| 246 | --color-clerk-foreground-muted: #737373; | ||
| 247 | --color-clerk-border: rgba(229, 229, 234, 0.06); | ||
| 248 | --color-clerk-danger: #dc2626; | ||
| 249 | } | ||
| 250 | |||
| 251 | @variant dark { | ||
| 252 | --color-clerk-page: #0e0e0e; | ||
| 253 | --color-clerk-foreground: #f5f5f5; | ||
| 254 | --color-clerk-foreground-muted: #a3a3a3; | ||
| 255 | --color-clerk-border: rgba(42, 42, 42, 0.06); | ||
| 256 | --color-clerk-danger: #fca5a5; | ||
| 257 | } | ||
| 258 | } | ||
| 259 | } | ||
| 260 | |||
| 236 | /* ─── Typography ────────────────────────────────────────────────────── */ | 261 | /* ─── Typography ────────────────────────────────────────────────────── */ |
| 237 | @theme { | 262 | @theme { |
| 238 | /* Keep these native family names aligned with app.config.ts. */ | 263 | /* Keep these native family names aligned with app.config.ts. */ |
apps/mobile/src/features/cloud/T3ConnectProfilePage.tsx created+273| ... | @@ -0,0 +1,273 @@ | ||
| 1 | import { findErrorTraceId } from "@t3tools/client-runtime/errors"; | ||
| 2 | import { | ||
| 3 | isAtomCommandInterrupted, | ||
| 4 | squashAtomCommandFailure, | ||
| 5 | } from "@t3tools/client-runtime/state/runtime"; | ||
| 6 | import type { MenuAction } from "@react-native-menu/menu"; | ||
| 7 | import type { EnvironmentId } from "@t3tools/contracts"; | ||
| 8 | import type { RelayClientEnvironmentRecord } from "@t3tools/contracts/relay"; | ||
| 9 | import { type ReactNode, useRef, useState } from "react"; | ||
| 10 | import { | ||
| 11 | ActivityIndicator, | ||
| 12 | Alert, | ||
| 13 | Pressable, | ||
| 14 | RefreshControl, | ||
| 15 | ScrollView, | ||
| 16 | Text, | ||
| 17 | View, | ||
| 18 | } from "react-native"; | ||
| 19 | |||
| 20 | import { SymbolView } from "../../components/AppSymbol"; | ||
| 21 | import { showConfirmDialog } from "../../components/ConfirmDialogHost"; | ||
| 22 | import { ControlPillMenu } from "../../components/ControlPill"; | ||
| 23 | import { copyTextWithHaptic } from "../../lib/copyTextWithHaptic"; | ||
| 24 | import { useAtomCommand } from "../../state/use-atom-command"; | ||
| 25 | import { | ||
| 26 | deregisterManagedRelayEnvironmentCommand, | ||
| 27 | useManagedRelayEnvironments, | ||
| 28 | } from "./managedRelayState"; | ||
| 29 | |||
| 30 | const linkedAtFormatter = new Intl.DateTimeFormat(undefined, { dateStyle: "medium" }); | ||
| 31 | |||
| 32 | function linkedAtLabel(value: string): string { | ||
| 33 | const linkedAt = new Date(value); | ||
| 34 | return Number.isNaN(linkedAt.getTime()) | ||
| 35 | ? "Link date unavailable" | ||
| 36 | : `Linked ${linkedAtFormatter.format(linkedAt)}`; | ||
| 37 | } | ||
| 38 | |||
| 39 | function endpointLabel(environment: RelayClientEnvironmentRecord): string { | ||
| 40 | return environment.endpoint.providerKind === "cloudflare_tunnel" | ||
| 41 | ? "Managed tunnel" | ||
| 42 | : "Activity publishing only"; | ||
| 43 | } | ||
| 44 | |||
| 45 | function confirmDeregister(environment: RelayClientEnvironmentRecord, onConfirm: () => void) { | ||
| 46 | const title = "Deregister server?"; | ||
| 47 | const message = `“${environment.label}” will be removed from this account. T3 Connect access will be revoked, any managed tunnel will be removed, and a host space will become available. Local connections on your devices are not changed.`; | ||
| 48 | if (process.env.EXPO_OS === "ios") { | ||
| 49 | Alert.alert(title, message, [ | ||
| 50 | { text: "Cancel", style: "cancel" }, | ||
| 51 | { text: "Deregister", style: "destructive", onPress: onConfirm }, | ||
| 52 | ]); | ||
| 53 | return; | ||
| 54 | } | ||
| 55 | showConfirmDialog({ title, message, confirmText: "Deregister", destructive: true, onConfirm }); | ||
| 56 | } | ||
| 57 | |||
| 58 | /** | ||
| 59 | * The "T3 Connect" custom page inside Clerk's native user profile: every | ||
| 60 | * environment registered to the signed-in account, with account-level | ||
| 61 | * deregistration. Mirrors the web UserButton page; connections on this device | ||
| 62 | * are managed in Settings instead. | ||
| 63 | */ | ||
| 64 | export function T3ConnectProfilePage() { | ||
| 65 | const environmentsState = useManagedRelayEnvironments(); | ||
| 66 | const deregisterEnvironment = useAtomCommand(deregisterManagedRelayEnvironmentCommand, { | ||
| 67 | reportFailure: false, | ||
| 68 | }); | ||
| 69 | const [deregisteringEnvironmentId, setDeregisteringEnvironmentId] = | ||
| 70 | useState<EnvironmentId | null>(null); | ||
| 71 | const mutationPendingRef = useRef(false); | ||
| 72 | // Deregistered rows stay in the cached list until the refresh lands, so hide | ||
| 73 | // them by the linkedAt they had. A re-link produces a new linkedAt and shows again. | ||
| 74 | const [removedEnvironments, setRemovedEnvironments] = useState<{ | ||
| 75 | readonly accountId: string | null; | ||
| 76 | readonly linkedAtById: ReadonlyMap<EnvironmentId, string>; | ||
| 77 | }>({ accountId: null, linkedAtById: new Map() }); | ||
| 78 | |||
| 79 | const handleDeregister = async (environment: RelayClientEnvironmentRecord) => { | ||
| 80 | const accountId = environmentsState.accountId; | ||
| 81 | if (!accountId || mutationPendingRef.current) return; | ||
| 82 | |||
| 83 | mutationPendingRef.current = true; | ||
| 84 | setDeregisteringEnvironmentId(environment.environmentId); | ||
| 85 | const result = await deregisterEnvironment({ | ||
| 86 | accountId, | ||
| 87 | environmentId: environment.environmentId, | ||
| 88 | }); | ||
| 89 | mutationPendingRef.current = false; | ||
| 90 | setDeregisteringEnvironmentId(null); | ||
| 91 | |||
| 92 | if (result._tag === "Success") { | ||
| 93 | setRemovedEnvironments((current) => { | ||
| 94 | const linkedAtById = new Map(current.accountId === accountId ? current.linkedAtById : []); | ||
| 95 | linkedAtById.set(environment.environmentId, environment.linkedAt); | ||
| 96 | return { accountId, linkedAtById }; | ||
| 97 | }); | ||
| 98 | environmentsState.refresh(); | ||
| 99 | return; | ||
| 100 | } | ||
| 101 | if (isAtomCommandInterrupted(result)) return; | ||
| 102 | |||
| 103 | const cause = squashAtomCommandFailure(result); | ||
| 104 | const message = cause instanceof Error ? cause.message : "Could not deregister the server."; | ||
| 105 | const traceId = findErrorTraceId(cause); | ||
| 106 | console.error("[t3-connect] Could not deregister environment", { | ||
| 107 | environmentId: environment.environmentId, | ||
| 108 | message, | ||
| 109 | traceId, | ||
| 110 | cause, | ||
| 111 | }); | ||
| 112 | Alert.alert( | ||
| 113 | "Could not deregister server", | ||
| 114 | traceId ? `${message}\n\nTrace ID: ${traceId}` : message, | ||
| 115 | traceId | ||
| 116 | ? [ | ||
| 117 | { | ||
| 118 | text: "Copy trace ID", | ||
| 119 | onPress: () => copyTextWithHaptic(traceId, { target: "connection-trace-id" }), | ||
| 120 | }, | ||
| 121 | { text: "OK", style: "cancel" }, | ||
| 122 | ] | ||
| 123 | : undefined, | ||
| 124 | ); | ||
| 125 | }; | ||
| 126 | |||
| 127 | const removedEnvironmentLinkedAt = | ||
| 128 | removedEnvironments.accountId === environmentsState.accountId | ||
| 129 | ? removedEnvironments.linkedAtById | ||
| 130 | : new Map<EnvironmentId, string>(); | ||
| 131 | const environments = (environmentsState.data ?? []).filter( | ||
| 132 | (environment) => | ||
| 133 | removedEnvironmentLinkedAt.get(environment.environmentId) !== environment.linkedAt, | ||
| 134 | ); | ||
| 135 | const isInitialLoad = | ||
| 136 | !environmentsState.accountId || (environmentsState.data === null && !environmentsState.error); | ||
| 137 | const errorTraceId = environmentsState.errorTraceId; | ||
| 138 | |||
| 139 | return ( | ||
| 140 | <ScrollView | ||
| 141 | className="flex-1 bg-clerk-page" | ||
| 142 | contentContainerClassName="pb-8" | ||
| 143 | contentInsetAdjustmentBehavior="automatic" | ||
| 144 | refreshControl={ | ||
| 145 | <RefreshControl | ||
| 146 | refreshing={environmentsState.isPending && environmentsState.data !== null} | ||
| 147 | onRefresh={environmentsState.refresh} | ||
| 148 | /> | ||
| 149 | } | ||
| 150 | > | ||
| 151 | <ClerkSectionHeader>Registered servers</ClerkSectionHeader> | ||
| 152 | |||
| 153 | {environmentsState.error ? ( | ||
| 154 | <> | ||
| 155 | <ClerkRow | ||
| 156 | title="Could not load T3 Connect environments" | ||
| 157 | subtitle={environmentsState.error} | ||
| 158 | /> | ||
| 159 | {errorTraceId ? ( | ||
| 160 | <ClerkButtonRow | ||
| 161 | label="Copy trace ID" | ||
| 162 | onPress={() => { | ||
| 163 | copyTextWithHaptic(errorTraceId, { target: "connection-trace-id" }); | ||
| 164 | }} | ||
| 165 | /> | ||
| 166 | ) : null} | ||
| 167 | </> | ||
| 168 | ) : isInitialLoad ? ( | ||
| 169 | <View className="flex-row items-center gap-3 px-6 py-4"> | ||
| 170 | <ActivityIndicator colorClassName={"accent-clerk-foreground-muted"} size="small" /> | ||
| 171 | <Text className="text-base text-clerk-foreground-muted">Loading environments</Text> | ||
| 172 | </View> | ||
| 173 | ) : environments.length > 0 ? ( | ||
| 174 | environments.map((environment) => ( | ||
| 175 | <ClerkRow | ||
| 176 | key={environment.environmentId} | ||
| 177 | title={environment.label} | ||
| 178 | subtitle={`${linkedAtLabel(environment.linkedAt)} · ${endpointLabel(environment)}`} | ||
| 179 | accessory={ | ||
| 180 | deregisteringEnvironmentId === environment.environmentId ? ( | ||
| 181 | <ActivityIndicator colorClassName={"accent-clerk-foreground-muted"} size="small" /> | ||
| 182 | ) : ( | ||
| 183 | <ControlPillMenu | ||
| 184 | actions={ENVIRONMENT_MENU_ACTIONS} | ||
| 185 | isAnchoredToRight | ||
| 186 | onPressAction={() => | ||
| 187 | confirmDeregister(environment, () => void handleDeregister(environment)) | ||
| 188 | } | ||
| 189 | > | ||
| 190 | <Pressable | ||
| 191 | accessibilityLabel={`Actions for ${environment.label}`} | ||
| 192 | accessibilityRole="button" | ||
| 193 | disabled={deregisteringEnvironmentId !== null} | ||
| 194 | className="size-[30px] items-center justify-center active:opacity-60 disabled:opacity-50" | ||
| 195 | > | ||
| 196 | <View className="rotate-90"> | ||
| 197 | <SymbolView | ||
| 198 | name="ellipsis" | ||
| 199 | size={18} | ||
| 200 | tintColorClassName={"accent-clerk-foreground-muted"} | ||
| 201 | type="monochrome" | ||
| 202 | /> | ||
| 203 | </View> | ||
| 204 | </Pressable> | ||
| 205 | </ControlPillMenu> | ||
| 206 | ) | ||
| 207 | } | ||
| 208 | /> | ||
| 209 | )) | ||
| 210 | ) : ( | ||
| 211 | <ClerkRow | ||
| 212 | title="No servers registered" | ||
| 213 | subtitle="Link a server from its local Settings to reach it through T3 Connect." | ||
| 214 | /> | ||
| 215 | )} | ||
| 216 | |||
| 217 | <Text className="px-6 pt-6 text-xs leading-normal text-clerk-foreground-muted"> | ||
| 218 | Connections on this device are managed in Settings. | ||
| 219 | </Text> | ||
| 220 | </ScrollView> | ||
| 221 | ); | ||
| 222 | } | ||
| 223 | |||
| 224 | const ENVIRONMENT_MENU_ACTIONS = [ | ||
| 225 | { id: "deregister", title: "Deregister", image: "trash", attributes: { destructive: true } }, | ||
| 226 | ] satisfies MenuAction[]; | ||
| 227 | |||
| 228 | // Layout primitives that mirror clerk-ios ClerkKitUI's profile rows so a custom | ||
| 229 | // page reads as one of Clerk's own screens. System font on purpose: Clerk's | ||
| 230 | // native views do not use the app's DM Sans. | ||
| 231 | |||
| 232 | function ClerkSectionHeader(props: { readonly children: string }) { | ||
| 233 | return ( | ||
| 234 | <Text className="min-h-4 border-b border-clerk-border px-6 pt-8 pb-4 text-xs font-medium tracking-[0.3px] text-clerk-foreground-muted uppercase"> | ||
| 235 | {props.children} | ||
| 236 | </Text> | ||
| 237 | ); | ||
| 238 | } | ||
| 239 | |||
| 240 | function ClerkRow(props: { | ||
| 241 | readonly title: string; | ||
| 242 | readonly subtitle: string; | ||
| 243 | readonly accessory?: ReactNode; | ||
| 244 | }) { | ||
| 245 | return ( | ||
| 246 | <View | ||
| 247 | collapsable={false} | ||
| 248 | className="flex-row items-center gap-3 border-b border-clerk-border px-6 py-4" | ||
| 249 | > | ||
| 250 | <View className="min-w-0 flex-1 gap-0.5"> | ||
| 251 | <Text className="min-h-[22px] text-base text-clerk-foreground" numberOfLines={1}> | ||
| 252 | {props.title} | ||
| 253 | </Text> | ||
| 254 | <Text className="min-h-5 text-sm text-clerk-foreground-muted" numberOfLines={2}> | ||
| 255 | {props.subtitle} | ||
| 256 | </Text> | ||
| 257 | </View> | ||
| 258 | {props.accessory} | ||
| 259 | </View> | ||
| 260 | ); | ||
| 261 | } | ||
| 262 | |||
| 263 | function ClerkButtonRow(props: { readonly label: string; readonly onPress: () => void }) { | ||
| 264 | return ( | ||
| 265 | <Pressable | ||
| 266 | accessibilityRole="button" | ||
| 267 | onPress={props.onPress} | ||
| 268 | className="border-b border-clerk-border px-6 py-4 active:opacity-60" | ||
| 269 | > | ||
| 270 | <Text className="text-base font-semibold text-clerk-foreground">{props.label}</Text> | ||
| 271 | </Pressable> | ||
| 272 | ); | ||
| 273 | } | ||
apps/mobile/src/features/cloud/managedRelayState.ts+22| ... | @@ -1,9 +1,15 @@ | ... | @@ -1,9 +1,15 @@ |
| 1 | import { useAtomValue } from "@effect/atom-react"; | 1 | import { useAtomValue } from "@effect/atom-react"; |
| 2 | import { | 2 | import { |
| 3 | createManagedRelayQueryManager, | 3 | createManagedRelayQueryManager, |
| 4 | deregisterManagedRelayEnvironment, | ||
| 4 | managedRelaySessionAtom, | 5 | managedRelaySessionAtom, |
| 5 | readManagedRelaySnapshotState, | 6 | readManagedRelaySnapshotState, |
| 6 | } from "@t3tools/client-runtime/relay"; | 7 | } from "@t3tools/client-runtime/relay"; |
| 8 | import { | ||
| 9 | createAtomCommandScheduler, | ||
| 10 | createRuntimeCommand, | ||
| 11 | } from "@t3tools/client-runtime/state/runtime"; | ||
| 12 | import type { EnvironmentId } from "@t3tools/contracts"; | ||
| 7 | import type { RelayClientEnvironmentRecord } from "@t3tools/contracts/relay"; | 13 | import type { RelayClientEnvironmentRecord } from "@t3tools/contracts/relay"; |
| 8 | import { AsyncResult, Atom } from "effect/unstable/reactivity"; | 14 | import { AsyncResult, Atom } from "effect/unstable/reactivity"; |
| 9 | import { useCallback, useEffect } from "react"; | 15 | import { useCallback, useEffect } from "react"; |
| ... | @@ -19,6 +25,22 @@ export const managedRelayQueryManager = createManagedRelayQueryManager(managedRe | ... | @@ -19,6 +25,22 @@ export const managedRelayQueryManager = createManagedRelayQueryManager(managedRe |
| 19 | cloudDebugLog(`query:${event.operation}:${event.stage}:${event.phase}`, { ...event }), | 25 | cloudDebugLog(`query:${event.operation}:${event.stage}:${event.phase}`, { ...event }), |
| 20 | }); | 26 | }); |
| 21 | 27 | ||
| 28 | const managedRelayMutationScheduler = createAtomCommandScheduler(); | ||
| 29 | |||
| 30 | export const deregisterManagedRelayEnvironmentCommand = createRuntimeCommand( | ||
| 31 | managedRelayAtomRuntime, | ||
| 32 | { | ||
| 33 | label: "mobile:managed-relay:deregister-environment", | ||
| 34 | scheduler: managedRelayMutationScheduler, | ||
| 35 | concurrency: { | ||
| 36 | mode: "serial", | ||
| 37 | key: (input: { readonly accountId: string; readonly environmentId: EnvironmentId }) => | ||
| 38 | input.accountId, | ||
| 39 | }, | ||
| 40 | execute: (input, registry) => deregisterManagedRelayEnvironment(registry, input), | ||
| 41 | }, | ||
| 42 | ); | ||
| 43 | |||
| 22 | const EMPTY_ENVIRONMENTS_ATOM = Atom.make( | 44 | const EMPTY_ENVIRONMENTS_ATOM = Atom.make( |
| 23 | AsyncResult.success<ReadonlyArray<RelayClientEnvironmentRecord>>([]), | 45 | AsyncResult.success<ReadonlyArray<RelayClientEnvironmentRecord>>([]), |
| 24 | ).pipe(Atom.keepAlive, Atom.withLabel("managed-relay:mobile:environments:null")); | 46 | ).pipe(Atom.keepAlive, Atom.withLabel("managed-relay:mobile:environments:null")); |
apps/mobile/src/features/settings/SettingsAuthRouteScreen.tsx+17-2| ... | @@ -1,10 +1,21 @@ | ... | @@ -1,10 +1,21 @@ |
| 1 | import { useAuth } from "@clerk/expo"; | 1 | import { useAuth } from "@clerk/expo"; |
| 2 | import { AuthView, UserProfileView } from "@clerk/expo/native"; | 2 | import { AuthView, type UserProfileCustomPage, UserProfileView } from "@clerk/expo/native"; |
| 3 | import { StackActions, useNavigation } from "@react-navigation/native"; | 3 | import { StackActions, useNavigation } from "@react-navigation/native"; |
| 4 | import { useCallback, useEffect, useLayoutEffect, useRef } from "react"; | 4 | import { useCallback, useEffect, useLayoutEffect, useRef } from "react"; |
| 5 | import { View } from "react-native"; | 5 | import { View } from "react-native"; |
| 6 | 6 | ||
| 7 | import { hasCloudPublicConfig } from "../cloud/publicConfig"; | 7 | import { hasCloudPublicConfig } from "../cloud/publicConfig"; |
| 8 | import { T3ConnectProfilePage } from "../cloud/T3ConnectProfilePage"; | ||
| 9 | |||
| 10 | // Custom rows in Clerk's native profile. Mirrors the web UserButton pages. | ||
| 11 | const USER_PROFILE_CUSTOM_PAGES = [ | ||
| 12 | { | ||
| 13 | path: "t3-connect", | ||
| 14 | label: "T3 Connect", | ||
| 15 | icon: "globe", | ||
| 16 | content: <T3ConnectProfilePage />, | ||
| 17 | }, | ||
| 18 | ] satisfies UserProfileCustomPage[]; | ||
| 8 | 19 | ||
| 9 | export function SettingsAuthRouteScreen() { | 20 | export function SettingsAuthRouteScreen() { |
| 10 | const navigation = useNavigation(); | 21 | const navigation = useNavigation(); |
| ... | @@ -40,7 +51,11 @@ function ConfiguredSettingsAuthRouteScreen() { | ... | @@ -40,7 +51,11 @@ function ConfiguredSettingsAuthRouteScreen() { |
| 40 | <View collapsable={false} className="flex-1 overflow-hidden bg-sheet"> | 51 | <View collapsable={false} className="flex-1 overflow-hidden bg-sheet"> |
| 41 | {isLoaded ? ( | 52 | {isLoaded ? ( |
| 42 | hasBeenSignedIn.current ? ( | 53 | hasBeenSignedIn.current ? ( |
| 43 | <UserProfileView isDismissible={false} onHostBack={handleHostBack} /> | 54 | <UserProfileView |
| 55 | customPages={USER_PROFILE_CUSTOM_PAGES} | ||
| 56 | isDismissible={false} | ||
| 57 | onHostBack={handleHostBack} | ||
| 58 | /> | ||
| 44 | ) : ( | 59 | ) : ( |
| 45 | <AuthView isDismissible={false} onHostBack={handleHostBack} /> | 60 | <AuthView isDismissible={false} onHostBack={handleHostBack} /> |
| 46 | ) | 61 | ) |
patches/@clerk__expo@4.6.6.patch+32-5| ... | @@ -1,8 +1,8 @@ | ... | @@ -1,8 +1,8 @@ |
| 1 | diff --git a/ios/ClerkAuthNativeView.swift b/ios/ClerkAuthNativeView.swift | 1 | diff --git a/ios/ClerkAuthNativeView.swift b/ios/ClerkAuthNativeView.swift |
| 2 | index e76a8be1b1c8faa64ec6dfa83764764094133aff..17b36ad1319765e2b6db0551d32e07d7140e482f 100644 | 2 | index 5f8d2ba66d5b232890af75c22b173c86d2ef546b..b170304cbf2fbaae8032db8dd6f89b6a59c6e3fe 100644 |
| 3 | --- a/ios/ClerkAuthNativeView.swift | 3 | --- a/ios/ClerkAuthNativeView.swift |
| 4 | +++ b/ios/ClerkAuthNativeView.swift | 4 | +++ b/ios/ClerkAuthNativeView.swift |
| 5 | @@ -108,7 +108,12 @@ public class ClerkAuthNativeView: ClerkNativeViewHost { | 5 | @@ -111,7 +111,12 @@ public class ClerkAuthNativeView: ClerkNativeViewHost { |
| 6 | 6 | ||
| 7 | override func makeHostedController() -> UIViewController? { | 7 | override func makeHostedController() -> UIViewController? { |
| 8 | let hostBackAction: (() -> Void)? = currentHostBackButton | 8 | let hostBackAction: (() -> Void)? = currentHostBackButton |
| ... | @@ -16,8 +16,35 @@ index e76a8be1b1c8faa64ec6dfa83764764094133aff..17b36ad1319765e2b6db0551d32e07d7 | ... | @@ -16,8 +16,35 @@ index e76a8be1b1c8faa64ec6dfa83764764094133aff..17b36ad1319765e2b6db0551d32e07d7 |
| 16 | : nil | 16 | : nil |
| 17 | 17 | ||
| 18 | return ClerkNativeBridge.shared.makeAuthViewController( | 18 | return ClerkNativeBridge.shared.makeAuthViewController( |
| 19 | diff --git a/ios/ClerkNativeBridge.swift b/ios/ClerkNativeBridge.swift | ||
| 20 | index c90ad2ef1162cc2ac025c8b0b4aa135e8913dc21..ce8cb68764b1a3d3537672fc83e4e3077fceb97a 100644 | ||
| 21 | --- a/ios/ClerkNativeBridge.swift | ||
| 22 | +++ b/ios/ClerkNativeBridge.swift | ||
| 23 | @@ -1470,6 +1470,8 @@ private struct ClerkReactEmbeddedUserProfileCustomPage: View { | ||
| 24 | } | ||
| 25 | |||
| 26 | private struct ClerkReactUserProfileCustomPageContent: View { | ||
| 27 | + @Environment(\.clerkTheme) private var theme | ||
| 28 | + | ||
| 29 | let path: String | ||
| 30 | let rows: [ClerkUserProfileCustomRowConfig] | ||
| 31 | let state: ClerkUserProfileCustomPageState | ||
| 32 | @@ -1483,8 +1485,13 @@ private struct ClerkReactUserProfileCustomPageContent: View { | ||
| 33 | } | ||
| 34 | } | ||
| 35 | .frame(maxWidth: .infinity, maxHeight: .infinity) | ||
| 36 | + // Match Clerk's own profile pages so the navigation bar and page share | ||
| 37 | + // the theme's page background instead of a bare hosting container. | ||
| 38 | + .background(theme.colors.muted) | ||
| 39 | .navigationTitle(userProfileCustomPageLabel(for: path, rows: rows)) | ||
| 40 | .navigationBarTitleDisplayMode(.inline) | ||
| 41 | + .preGlassSolidNavBar() | ||
| 42 | + .background(theme.colors.background) | ||
| 43 | } | ||
| 44 | } | ||
| 45 | |||
| 19 | diff --git a/ios/ClerkNativeViewHost.swift b/ios/ClerkNativeViewHost.swift | 46 | diff --git a/ios/ClerkNativeViewHost.swift b/ios/ClerkNativeViewHost.swift |
| 20 | index 0d91f0e749f121595c17bc803663df6ac90e4163..8f8a89df97a54168b8b45d0e9c197ca3953b7028 100644 | 47 | index 4e6540aceeaaab56ddf327bb3f16e120438f066f..cc16c94ca7a6eb91a97e76d5de02dc586f02a766 100644 |
| 21 | --- a/ios/ClerkNativeViewHost.swift | 48 | --- a/ios/ClerkNativeViewHost.swift |
| 22 | +++ b/ios/ClerkNativeViewHost.swift | 49 | +++ b/ios/ClerkNativeViewHost.swift |
| 23 | @@ -5,6 +5,7 @@ public class ClerkNativeViewHost: ExpoView { | 50 | @@ -5,6 +5,7 @@ public class ClerkNativeViewHost: ExpoView { |
| ... | @@ -60,10 +87,10 @@ index 0d91f0e749f121595c17bc803663df6ac90e4163..8f8a89df97a54168b8b45d0e9c197ca3 | ... | @@ -60,10 +87,10 @@ index 0d91f0e749f121595c17bc803663df6ac90e4163..8f8a89df97a54168b8b45d0e9c197ca3 |
| 60 | guard configuredObserver == nil else { return } | 87 | guard configuredObserver == nil else { return } |
| 61 | 88 | ||
| 62 | diff --git a/ios/ClerkUserProfileNativeView.swift b/ios/ClerkUserProfileNativeView.swift | 89 | diff --git a/ios/ClerkUserProfileNativeView.swift b/ios/ClerkUserProfileNativeView.swift |
| 63 | index 12d6248b1dc4b4779b252c9954c2f89145907310..d838283b7adcf49fec2a63bff53251b9bee31bf8 100644 | 90 | index f2ca604d41f50d05de8ac334879126cdc0414f84..46f406d5361b9a560a7c4ce855e344cbddd1d0b6 100644 |
| 64 | --- a/ios/ClerkUserProfileNativeView.swift | 91 | --- a/ios/ClerkUserProfileNativeView.swift |
| 65 | +++ b/ios/ClerkUserProfileNativeView.swift | 92 | +++ b/ios/ClerkUserProfileNativeView.swift |
| 66 | @@ -44,7 +44,12 @@ public class ClerkUserProfileNativeView: ClerkNativeViewHost { | 93 | @@ -47,7 +47,12 @@ public class ClerkUserProfileNativeView: ClerkUserProfileCustomPageHost { |
| 67 | 94 | ||
| 68 | override func makeHostedController() -> UIViewController? { | 95 | override func makeHostedController() -> UIViewController? { |
| 69 | let hostBackAction: (() -> Void)? = currentHostBackButton | 96 | let hostBackAction: (() -> Void)? = currentHostBackButton |
pnpm-lock.yaml+5-5| ... | @@ -85,7 +85,7 @@ overrides: | ... | @@ -85,7 +85,7 @@ overrides: |
| 85 | packageExtensionsChecksum: sha256-k/dT9NFDl5hihRPaoFKeY11hzyutMFs5psfZLFiKJic= | 85 | packageExtensionsChecksum: sha256-k/dT9NFDl5hihRPaoFKeY11hzyutMFs5psfZLFiKJic= |
| 86 | 86 | ||
| 87 | patchedDependencies: | 87 | patchedDependencies: |
| 88 | '@clerk/expo@4.6.6': 72e426f44fc1cde16fc2cbba3d1e96cdca7c6d957faa73d0fe6b43948608a6c1 | 88 | '@clerk/expo@4.6.6': a82bb41039ee88a290a87d4ab58be7be4c49fa338603b17ea646e1ec140a1cfb |
| 89 | '@effect/vitest@4.0.0-rc.112': a16b1e870d8c29e4a98b17cc4c638a4ff471753d8ca3487f78a22b759caf951b | 89 | '@effect/vitest@4.0.0-rc.112': a16b1e870d8c29e4a98b17cc4c638a4ff471753d8ca3487f78a22b759caf951b |
| 90 | '@expo/metro-config@57.0.12': 96f1a75347e6ea02dc4b7034ace815d8ee39e18b8166ebfb573d9e58328f0dc2 | 90 | '@expo/metro-config@57.0.12': 96f1a75347e6ea02dc4b7034ace815d8ee39e18b8166ebfb573d9e58328f0dc2 |
| 91 | '@ff-labs/fff-node@0.9.4': c4e3cc2420ceb9dc650f9d189e9c24baf7e83f342998bacda5f459a4ce7927a8 | 91 | '@ff-labs/fff-node@0.9.4': c4e3cc2420ceb9dc650f9d189e9c24baf7e83f342998bacda5f459a4ce7927a8 |
| ... | @@ -231,7 +231,7 @@ importers: | ... | @@ -231,7 +231,7 @@ importers: |
| 231 | dependencies: | 231 | dependencies: |
| 232 | '@clerk/expo': | 232 | '@clerk/expo': |
| 233 | specifier: 4.6.6 | 233 | specifier: 4.6.6 |
| 234 | version: 4.6.6(patch_hash=72e426f44fc1cde16fc2cbba3d1e96cdca7c6d957faa73d0fe6b43948608a6c1)(dbc31631339ce74330e188d5d7a88158) | 234 | version: 4.6.6(patch_hash=a82bb41039ee88a290a87d4ab58be7be4c49fa338603b17ea646e1ec140a1cfb)(dbc31631339ce74330e188d5d7a88158) |
| 235 | '@effect/atom-react': | 235 | '@effect/atom-react': |
| 236 | specifier: 4.0.0-rc.112 | 236 | specifier: 4.0.0-rc.112 |
| 237 | version: 4.0.0-rc.112(effect@4.0.0-rc.112(patch_hash=8bef799f35729cf3465eb428196f617b434a7c9f658d56acb2874d74b21b98b2))(react@19.2.3)(scheduler@0.27.0) | 237 | version: 4.0.0-rc.112(effect@4.0.0-rc.112(patch_hash=8bef799f35729cf3465eb428196f617b434a7c9f658d56acb2874d74b21b98b2))(react@19.2.3)(scheduler@0.27.0) |
| ... | @@ -774,7 +774,7 @@ importers: | ... | @@ -774,7 +774,7 @@ importers: |
| 774 | version: link:../../packages/shared | 774 | version: link:../../packages/shared |
| 775 | alchemy: | 775 | alchemy: |
| 776 | specifier: 2.0.0-beta.76 | 776 | specifier: 2.0.0-beta.76 |
| 777 | version: 2.0.0-beta.76(b3825b36417e56a486ccb5f22a7f3d7e) | 777 | version: 2.0.0-beta.76(197b68e0d20fdf20cf352009eca17c9f) |
| 778 | drizzle-orm: | 778 | drizzle-orm: |
| 779 | specifier: 1.0.0-rc.5-ab785fc | 779 | specifier: 1.0.0-rc.5-ab785fc |
| 780 | version: 1.0.0-rc.5-ab785fc(8ab70e2706da13c78d64d8a92fef1884) | 780 | version: 1.0.0-rc.5-ab785fc(8ab70e2706da13c78d64d8a92fef1884) |
| ... | @@ -12437,7 +12437,7 @@ snapshots: | ... | @@ -12437,7 +12437,7 @@ snapshots: |
| 12437 | electron-store: 8.2.0 | 12437 | electron-store: 8.2.0 |
| 12438 | react-dom: 19.2.6(react@19.2.6) | 12438 | react-dom: 19.2.6(react@19.2.6) |
| 12439 | 12439 | ||
| 12440 | '@clerk/expo@4.6.6(patch_hash=72e426f44fc1cde16fc2cbba3d1e96cdca7c6d957faa73d0fe6b43948608a6c1)(dbc31631339ce74330e188d5d7a88158)': | 12440 | '@clerk/expo@4.6.6(patch_hash=a82bb41039ee88a290a87d4ab58be7be4c49fa338603b17ea646e1ec140a1cfb)(dbc31631339ce74330e188d5d7a88158)': |
| 12441 | dependencies: | 12441 | dependencies: |
| 12442 | '@clerk/clerk-js': 6.31.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) | 12442 | '@clerk/clerk-js': 6.31.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) |
| 12443 | '@clerk/react': 6.15.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) | 12443 | '@clerk/react': 6.15.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) |
| ... | @@ -16445,7 +16445,7 @@ snapshots: | ... | @@ -16445,7 +16445,7 @@ snapshots: |
| 16445 | json-schema-traverse: 1.0.0 | 16445 | json-schema-traverse: 1.0.0 |
| 16446 | require-from-string: 2.0.2 | 16446 | require-from-string: 2.0.2 |
| 16447 | 16447 | ||
| 16448 | alchemy@2.0.0-beta.76(b3825b36417e56a486ccb5f22a7f3d7e): | 16448 | alchemy@2.0.0-beta.76(197b68e0d20fdf20cf352009eca17c9f): |
| 16449 | dependencies: | 16449 | dependencies: |
| 16450 | '@alchemy.run/cloudflare-runtime': 2.0.0-beta.76(@distilled.cloud/cloudflare@1.0.0-rc.8(effect@4.0.0-rc.112(patch_hash=8bef799f35729cf3465eb428196f617b434a7c9f658d56acb2874d74b21b98b2)))(@effect/platform-bun@4.0.0-rc.112(bufferutil@4.1.0)(effect@4.0.0-rc.112(patch_hash=8bef799f35729cf3465eb428196f617b434a7c9f658d56acb2874d74b21b98b2))(utf-8-validate@6.0.6))(@effect/platform-node@4.0.0-rc.112(bufferutil@4.1.0)(effect@4.0.0-rc.112(patch_hash=8bef799f35729cf3465eb428196f617b434a7c9f658d56acb2874d74b21b98b2))(redis@6.2.1)(utf-8-validate@6.0.6))(@types/node@24.12.4)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(effect@4.0.0-rc.112(patch_hash=8bef799f35729cf3465eb428196f617b434a7c9f658d56acb2874d74b21b98b2))(rolldown@1.2.5)(typescript@7.0.2) | 16450 | '@alchemy.run/cloudflare-runtime': 2.0.0-beta.76(@distilled.cloud/cloudflare@1.0.0-rc.8(effect@4.0.0-rc.112(patch_hash=8bef799f35729cf3465eb428196f617b434a7c9f658d56acb2874d74b21b98b2)))(@effect/platform-bun@4.0.0-rc.112(bufferutil@4.1.0)(effect@4.0.0-rc.112(patch_hash=8bef799f35729cf3465eb428196f617b434a7c9f658d56acb2874d74b21b98b2))(utf-8-validate@6.0.6))(@effect/platform-node@4.0.0-rc.112(bufferutil@4.1.0)(effect@4.0.0-rc.112(patch_hash=8bef799f35729cf3465eb428196f617b434a7c9f658d56acb2874d74b21b98b2))(redis@6.2.1)(utf-8-validate@6.0.6))(@types/node@24.12.4)(@voidzero-dev/vite-plus-core@0.3.0(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@7.0.2)(unrun@0.2.39)(yaml@2.9.0))(effect@4.0.0-rc.112(patch_hash=8bef799f35729cf3465eb428196f617b434a7c9f658d56acb2874d74b21b98b2))(rolldown@1.2.5)(typescript@7.0.2) |
| 16451 | '@alchemy.run/floci': 2.0.0-beta.76(effect@4.0.0-rc.112(patch_hash=8bef799f35729cf3465eb428196f617b434a7c9f658d56acb2874d74b21b98b2)) | 16451 | '@alchemy.run/floci': 2.0.0-beta.76(effect@4.0.0-rc.112(patch_hash=8bef799f35729cf3465eb428196f617b434a7c9f658d56acb2874d74b21b98b2)) |