| 1 | import type { ExpoConfig } from "expo/config"; |
| 2 | |
| 3 | import { BRAND_ASSET_PATHS } from "../../scripts/lib/brand-assets.ts"; |
| 4 | import { loadRepoEnv } from "../../scripts/lib/public-config.ts"; |
| 5 | |
| 6 | type AppVariant = "development" | "preview" | "production"; |
| 7 | |
| 8 | const repoEnv = loadRepoEnv(); |
| 9 | Object.assign(process.env, repoEnv); |
| 10 | |
| 11 | const APP_VARIANT = resolveAppVariant(repoEnv.APP_VARIANT); |
| 12 | const isIosPersonalTeamBuild = repoEnv.T3CODE_IOS_PERSONAL_TEAM === "1"; |
| 13 | const runtimeVersionPolicy = |
| 14 | process.env.MOBILE_VERSION_POLICY ?? |
| 15 | (APP_VARIANT === "development" ? "appVersion" : "fingerprint"); |
| 16 | |
| 17 | const personalTeamBundleIdentifier = repoEnv.T3CODE_IOS_PERSONAL_TEAM_BUNDLE_ID?.trim(); |
| 18 | const IOS_BUNDLE_IDENTIFIER_PATTERN = /^[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)+$/; |
| 19 | |
| 20 | const fromRepoRoot = (relativePath: string) => `../../${relativePath}`; |
| 21 | // Android layers are rendered by scripts/export-android-icons.ts from the Icon Composer sources. |
| 22 | // The knife sits inside the adaptive icon safe zone. |
| 23 | const androidAdaptiveForeground = "../../assets/knife/android-foreground.png"; |
| 24 | |
| 25 | if ( |
| 26 | isIosPersonalTeamBuild && |
| 27 | (!personalTeamBundleIdentifier || |
| 28 | !IOS_BUNDLE_IDENTIFIER_PATTERN.test(personalTeamBundleIdentifier)) |
| 29 | ) { |
| 30 | throw new Error( |
| 31 | "T3CODE_IOS_PERSONAL_TEAM_BUNDLE_ID must be a reverse-DNS identifier such as com.example.t3code when T3CODE_IOS_PERSONAL_TEAM=1.", |
| 32 | ); |
| 33 | } |
| 34 | |
| 35 | const DEVELOPMENT_ASSETS = { |
| 36 | appIcon: fromRepoRoot(BRAND_ASSET_PATHS.developmentIosIconPng), |
| 37 | iosIcon: fromRepoRoot(BRAND_ASSET_PATHS.developmentIconComposerProject), |
| 38 | splashIcon: fromRepoRoot(BRAND_ASSET_PATHS.developmentIosIconPng), |
| 39 | androidAdaptiveForeground, |
| 40 | androidAdaptiveBackgroundColor: "#171422", |
| 41 | androidAdaptiveBackgroundImage: undefined, |
| 42 | androidSplashIcon: "../../assets/knife/android-splash.png", |
| 43 | androidMonochromeIcon: "../../assets/knife/android-foreground.png", |
| 44 | androidNotificationIcon: "../../assets/knife/android-foreground.png", |
| 45 | androidNotificationColor: "#00639B", |
| 46 | } as const; |
| 47 | |
| 48 | const PREVIEW_ASSETS = { |
| 49 | appIcon: fromRepoRoot(BRAND_ASSET_PATHS.nightlyIosIconPng), |
| 50 | iosIcon: fromRepoRoot(BRAND_ASSET_PATHS.nightlyIconComposerProject), |
| 51 | splashIcon: fromRepoRoot(BRAND_ASSET_PATHS.nightlyIosIconPng), |
| 52 | androidAdaptiveForeground, |
| 53 | androidAdaptiveBackgroundColor: "#171422", |
| 54 | androidAdaptiveBackgroundImage: undefined, |
| 55 | androidSplashIcon: "../../assets/knife/android-splash.png", |
| 56 | androidMonochromeIcon: "../../assets/knife/android-foreground.png", |
| 57 | androidNotificationIcon: "../../assets/knife/android-foreground.png", |
| 58 | androidNotificationColor: "#7565C7", |
| 59 | } as const; |
| 60 | |
| 61 | const RELEASE_ASSETS = { |
| 62 | appIcon: fromRepoRoot(BRAND_ASSET_PATHS.productionIosIconPng), |
| 63 | iosIcon: fromRepoRoot(BRAND_ASSET_PATHS.productionIconComposerProject), |
| 64 | splashIcon: fromRepoRoot(BRAND_ASSET_PATHS.productionIosIconPng), |
| 65 | androidAdaptiveForeground, |
| 66 | androidAdaptiveBackgroundColor: "#171422", |
| 67 | androidAdaptiveBackgroundImage: undefined, |
| 68 | androidSplashIcon: "../../assets/knife/android-splash.png", |
| 69 | androidMonochromeIcon: "../../assets/knife/android-foreground.png", |
| 70 | androidNotificationIcon: "../../assets/knife/android-foreground.png", |
| 71 | androidNotificationColor: "#FFFFFF", |
| 72 | } as const; |
| 73 | |
| 74 | const VARIANT_CONFIG = { |
| 75 | development: { |
| 76 | appName: "The Knife Dev", |
| 77 | scheme: "t3code-dev", |
| 78 | iosBundleIdentifier: "com.t3tools.t3code.dev", |
| 79 | androidPackage: "com.t3tools.t3code.dev", |
| 80 | relyingParty: "clerk.t3.codes", |
| 81 | assets: DEVELOPMENT_ASSETS, |
| 82 | }, |
| 83 | preview: { |
| 84 | appName: "The Knife Preview", |
| 85 | scheme: "t3code-preview", |
| 86 | iosBundleIdentifier: "com.t3tools.t3code.preview", |
| 87 | androidPackage: "com.t3tools.t3code.preview", |
| 88 | relyingParty: "clerk.t3.codes", |
| 89 | assets: PREVIEW_ASSETS, |
| 90 | }, |
| 91 | production: { |
| 92 | appName: "The Knife", |
| 93 | scheme: "t3code", |
| 94 | iosBundleIdentifier: "com.t3tools.t3code", |
| 95 | androidPackage: "com.t3tools.t3code", |
| 96 | relyingParty: "clerk.t3.codes", |
| 97 | assets: RELEASE_ASSETS, |
| 98 | }, |
| 99 | } as const; |
| 100 | |
| 101 | function resolveAppVariant(value: string | undefined): AppVariant { |
| 102 | switch (value) { |
| 103 | case "development": |
| 104 | case "preview": |
| 105 | case "production": |
| 106 | return value; |
| 107 | default: |
| 108 | return "production"; |
| 109 | } |
| 110 | } |
| 111 | |
| 112 | const variant = VARIANT_CONFIG[APP_VARIANT]; |
| 113 | const iosBundleIdentifier = isIosPersonalTeamBuild |
| 114 | ? personalTeamBundleIdentifier! |
| 115 | : variant.iosBundleIdentifier; |
| 116 | |
| 117 | const dmSansFonts = { |
| 118 | regular: "@expo-google-fonts/dm-sans/400Regular/DMSans_400Regular.ttf", |
| 119 | medium: "@expo-google-fonts/dm-sans/500Medium/DMSans_500Medium.ttf", |
| 120 | bold: "@expo-google-fonts/dm-sans/700Bold/DMSans_700Bold.ttf", |
| 121 | } as const; |
| 122 | |
| 123 | const widgetsPlugin: NonNullable<ExpoConfig["plugins"]>[number] = [ |
| 124 | "expo-widgets", |
| 125 | { |
| 126 | bundleIdentifier: `${iosBundleIdentifier}.widgets`, |
| 127 | groupIdentifier: `group.${iosBundleIdentifier}`, |
| 128 | enablePushNotifications: true, |
| 129 | // Agent activity can update many times an hour; without the |
| 130 | // frequent-updates entitlement iOS throttles the update budget sooner. |
| 131 | frequentUpdates: true, |
| 132 | enableAndroid: true, |
| 133 | widgets: [ |
| 134 | { |
| 135 | name: "SubscriptionUsage", |
| 136 | displayName: "Subscription usage", |
| 137 | description: "Subscription quotas from your connected environments.", |
| 138 | ios: { |
| 139 | configuration: { |
| 140 | title: "Subscription usage", |
| 141 | description: |
| 142 | "Both shows Session and Weekly when available. The Lock Screen shows the tightest selected limit.", |
| 143 | parameters: { |
| 144 | codexPeriod: { |
| 145 | title: "Codex limits", |
| 146 | type: "enum", |
| 147 | default: "auto", |
| 148 | values: [ |
| 149 | { name: "Both", value: "auto" }, |
| 150 | { name: "Session", value: "session" }, |
| 151 | { name: "Weekly", value: "weekly" }, |
| 152 | ], |
| 153 | }, |
| 154 | claudePeriod: { |
| 155 | title: "Claude limits", |
| 156 | type: "enum", |
| 157 | default: "auto", |
| 158 | values: [ |
| 159 | { name: "Both", value: "auto" }, |
| 160 | { name: "Session", value: "session" }, |
| 161 | { name: "Weekly", value: "weekly" }, |
| 162 | ], |
| 163 | }, |
| 164 | }, |
| 165 | }, |
| 166 | supportedFamilies: [ |
| 167 | "systemSmall", |
| 168 | "systemMedium", |
| 169 | "systemLarge", |
| 170 | "systemExtraLarge", |
| 171 | "accessoryRectangular", |
| 172 | ], |
| 173 | }, |
| 174 | android: { |
| 175 | minWidth: 250, |
| 176 | minHeight: 180, |
| 177 | targetCellWidth: 4, |
| 178 | targetCellHeight: 3, |
| 179 | resizeMode: "both", |
| 180 | // Embeds the layout in the APK so the widget renders before the app |
| 181 | // has run once; the app replaces it with stored props on publish. |
| 182 | initialLayout: "./src/widgets/SubscriptionUsage.android.tsx", |
| 183 | }, |
| 184 | }, |
| 185 | { |
| 186 | name: "AgentActivity", |
| 187 | displayName: "Agent Activity", |
| 188 | description: "Shows the current state of your active agents.", |
| 189 | // Live Activity companion; there is no Android presentation for it. |
| 190 | android: null, |
| 191 | ios: { supportedFamilies: ["systemSmall", "systemMedium", "accessoryRectangular"] }, |
| 192 | }, |
| 193 | ], |
| 194 | }, |
| 195 | ]; |
| 196 | |
| 197 | const sharingPlugin: NonNullable<ExpoConfig["plugins"]>[number] = [ |
| 198 | "expo-sharing", |
| 199 | { |
| 200 | ios: { |
| 201 | // Personal Teams cannot sign App Groups or extension targets. Keep the |
| 202 | // reduced-capability local build usable while release builds expose the |
| 203 | // real system share target. |
| 204 | enabled: !isIosPersonalTeamBuild, |
| 205 | extensionBundleIdentifier: `${iosBundleIdentifier}.sharing`, |
| 206 | appGroupId: `group.${iosBundleIdentifier}`, |
| 207 | activationRule: { |
| 208 | supportsText: true, |
| 209 | supportsWebUrlWithMaxCount: 1, |
| 210 | supportsImageWithMaxCount: 8, |
| 211 | supportsMovieWithMaxCount: 8, |
| 212 | supportsFileWithMaxCount: 8, |
| 213 | }, |
| 214 | }, |
| 215 | android: { |
| 216 | enabled: true, |
| 217 | singleShareMimeTypes: ["*/*"], |
| 218 | multipleShareMimeTypes: ["*/*"], |
| 219 | }, |
| 220 | }, |
| 221 | ]; |
| 222 | |
| 223 | // These aliases match the fonts' PostScript names on iOS. Register the same |
| 224 | // names on Android so React Native and the native composer use one set of |
| 225 | // family names without waiting for runtime font loading. |
| 226 | |
| 227 | const config: ExpoConfig = { |
| 228 | name: variant.appName, |
| 229 | slug: "t3-code", |
| 230 | platforms: ["ios", "android"], |
| 231 | scheme: variant.scheme, |
| 232 | version: "2.0.0", |
| 233 | runtimeVersion: { |
| 234 | // Development manifests resolve on every launch, so avoid fingerprint's |
| 235 | // expensive native-project calculation there. Preview and production stay |
| 236 | // fingerprinted so OTAs only reach binaries with matching native projects. |
| 237 | policy: runtimeVersionPolicy, |
| 238 | }, |
| 239 | orientation: "portrait", |
| 240 | icon: variant.assets.appIcon, |
| 241 | userInterfaceStyle: "automatic", |
| 242 | updates: { |
| 243 | enabled: repoEnv.T3CODE_MOBILE_UPDATES_ENABLED !== "0", |
| 244 | url: "https://u.expo.dev/d763fcb8-d37c-41ea-a773-b54a0ab4a454", |
| 245 | checkAutomatically: "ON_LOAD", |
| 246 | fallbackToCacheTimeout: 0, |
| 247 | }, |
| 248 | ios: { |
| 249 | icon: variant.assets.iosIcon, |
| 250 | supportsTablet: true, |
| 251 | // Multitasking-capable iPad apps cannot rotate programmatically, so the |
| 252 | // showcase capture build requires full screen (see infoPlist below). |
| 253 | requireFullScreen: process.env.T3_SHOWCASE_CAPTURE_BUILD === "1", |
| 254 | bundleIdentifier: iosBundleIdentifier, |
| 255 | // Pin code signing to the T3 Tools team so non-interactive `expo run:ios` |
| 256 | // does not fall back to a personal team (which cannot sign app groups, |
| 257 | // Sign in with Apple, or push notification entitlements). |
| 258 | appleTeamId: "ARK85ZXQ4Z", |
| 259 | associatedDomains: [ |
| 260 | `applinks:${variant.relyingParty}`, |
| 261 | `webcredentials:${variant.relyingParty}`, |
| 262 | ], |
| 263 | entitlements: { |
| 264 | "keychain-access-groups": [`$(AppIdentifierPrefix)${iosBundleIdentifier}`], |
| 265 | }, |
| 266 | infoPlist: { |
| 267 | UISupportedInterfaceOrientations: [ |
| 268 | "UIInterfaceOrientationPortrait", |
| 269 | "UIInterfaceOrientationLandscapeLeft", |
| 270 | "UIInterfaceOrientationLandscapeRight", |
| 271 | ], |
| 272 | NSAppTransportSecurity: { |
| 273 | NSAllowsArbitraryLoads: true, |
| 274 | }, |
| 275 | NSLocalNetworkUsageDescription: |
| 276 | "Allow The Knife to connect to The Knife servers on your local network or tailnet.", |
| 277 | NSPhotoLibraryAddUsageDescription: "Allow The Knife to save images to your photo library.", |
| 278 | // "Audio, AirPlay, and Picture in Picture": the browser screen's system |
| 279 | // picture in picture needs it to start and to stay up outside the app. |
| 280 | UIBackgroundModes: ["audio"], |
| 281 | ITSAppUsesNonExemptEncryption: false, |
| 282 | // The App Store screenshot harness rotates the iPad interface from |
| 283 | // inside the app (CI denies osascript the Accessibility access that |
| 284 | // Simulator menu scripting needs), and iPadOS ignores programmatic |
| 285 | // orientation requests for multitasking-capable apps — so the capture |
| 286 | // build opts out of multitasking and declares landscape support. |
| 287 | ...(process.env.T3_SHOWCASE_CAPTURE_BUILD === "1" |
| 288 | ? { |
| 289 | "UISupportedInterfaceOrientations~ipad": [ |
| 290 | "UIInterfaceOrientationPortrait", |
| 291 | "UIInterfaceOrientationPortraitUpsideDown", |
| 292 | "UIInterfaceOrientationLandscapeLeft", |
| 293 | "UIInterfaceOrientationLandscapeRight", |
| 294 | ], |
| 295 | } |
| 296 | : {}), |
| 297 | }, |
| 298 | }, |
| 299 | android: { |
| 300 | icon: variant.assets.appIcon, |
| 301 | package: variant.androidPackage, |
| 302 | ...(repoEnv.T3CODE_ANDROID_GOOGLE_SERVICES_FILE |
| 303 | ? { googleServicesFile: repoEnv.T3CODE_ANDROID_GOOGLE_SERVICES_FILE } |
| 304 | : {}), |
| 305 | adaptiveIcon: { |
| 306 | backgroundColor: variant.assets.androidAdaptiveBackgroundColor, |
| 307 | ...(variant.assets.androidAdaptiveBackgroundImage |
| 308 | ? { backgroundImage: variant.assets.androidAdaptiveBackgroundImage } |
| 309 | : {}), |
| 310 | foregroundImage: variant.assets.androidAdaptiveForeground, |
| 311 | monochromeImage: variant.assets.androidMonochromeIcon, |
| 312 | }, |
| 313 | // Opts into OnBackInvokedCallback-based back dispatch (Android 13+). |
| 314 | // JS back handling survives it via react-native's Android 16 shim plus |
| 315 | // withAndroidPredictiveBackCompat on Android 13-15. |
| 316 | predictiveBackGestureEnabled: true, |
| 317 | // expo-sensors declares this for its pedometer, which the app does not use. |
| 318 | blockedPermissions: ["android.permission.ACTIVITY_RECOGNITION"], |
| 319 | }, |
| 320 | web: { |
| 321 | favicon: variant.assets.appIcon, |
| 322 | }, |
| 323 | plugins: [ |
| 324 | ...(isIosPersonalTeamBuild ? ["./plugins/withoutIosPersonalTeamCapabilities.cjs"] : []), |
| 325 | "expo-asset", |
| 326 | [ |
| 327 | "expo-font", |
| 328 | { |
| 329 | ios: { |
| 330 | fonts: [dmSansFonts.regular, dmSansFonts.medium, dmSansFonts.bold], |
| 331 | }, |
| 332 | android: { |
| 333 | fonts: [ |
| 334 | { |
| 335 | fontFamily: "DMSans-Regular", |
| 336 | fontDefinitions: [{ path: dmSansFonts.regular, weight: 400 }], |
| 337 | }, |
| 338 | { |
| 339 | fontFamily: "DMSans-Medium", |
| 340 | fontDefinitions: [{ path: dmSansFonts.medium, weight: 500 }], |
| 341 | }, |
| 342 | { |
| 343 | fontFamily: "DMSans-Bold", |
| 344 | fontDefinitions: [{ path: dmSansFonts.bold, weight: 700 }], |
| 345 | }, |
| 346 | ], |
| 347 | }, |
| 348 | }, |
| 349 | ], |
| 350 | "expo-secure-store", |
| 351 | "expo-sqlite", |
| 352 | ...(isIosPersonalTeamBuild |
| 353 | ? [sharingPlugin] |
| 354 | : ["./plugins/withShareExtensionDisplayName.cjs", sharingPlugin]), |
| 355 | [ |
| 356 | "expo-notifications", |
| 357 | { |
| 358 | icon: variant.assets.androidNotificationIcon, |
| 359 | color: variant.assets.androidNotificationColor, |
| 360 | mode: APP_VARIANT === "development" ? "development" : "production", |
| 361 | }, |
| 362 | ], |
| 363 | ["@clerk/expo", { theme: "./clerk-theme.json", appleSignIn: !isIosPersonalTeamBuild }], |
| 364 | "expo-web-browser", |
| 365 | [ |
| 366 | "expo-quick-actions", |
| 367 | { |
| 368 | // Adaptive launcher-shortcut icon; referenced by resource name from |
| 369 | // the shortcut items set in src/features/shortcuts. |
| 370 | androidIcons: { |
| 371 | shortcut_icon: { |
| 372 | foregroundImage: variant.assets.androidAdaptiveForeground, |
| 373 | backgroundColor: variant.assets.androidAdaptiveBackgroundColor, |
| 374 | ...(variant.assets.androidAdaptiveBackgroundImage |
| 375 | ? { backgroundImage: variant.assets.androidAdaptiveBackgroundImage } |
| 376 | : {}), |
| 377 | }, |
| 378 | }, |
| 379 | }, |
| 380 | ], |
| 381 | [ |
| 382 | "expo-audio", |
| 383 | { |
| 384 | microphonePermission: "Allow The Knife to use your microphone for voice input.", |
| 385 | recordAudioAndroid: false, |
| 386 | enableBackgroundPlayback: false, |
| 387 | enableBackgroundRecording: false, |
| 388 | }, |
| 389 | ], |
| 390 | [ |
| 391 | "expo-camera", |
| 392 | { |
| 393 | cameraPermission: "Allow The Knife to access your camera so you can scan pairing QR codes.", |
| 394 | microphonePermission: false, |
| 395 | barcodeScannerEnabled: true, |
| 396 | recordAudioAndroid: false, |
| 397 | }, |
| 398 | ], |
| 399 | ["expo-image-picker", { photosPermission: false, microphonePermission: false }], |
| 400 | [ |
| 401 | "expo-splash-screen", |
| 402 | { |
| 403 | image: variant.assets.splashIcon, |
| 404 | resizeMode: "contain", |
| 405 | backgroundColor: "#ffffff", |
| 406 | imageWidth: 220, |
| 407 | dark: { |
| 408 | image: variant.assets.splashIcon, |
| 409 | backgroundColor: "#0a0a0a", |
| 410 | }, |
| 411 | android: { |
| 412 | // Android 12+ masks the splash icon to a circle over the central two thirds of |
| 413 | // its 288dp canvas, so the iOS export's corners get cut. A full-canvas image of |
| 414 | // the composed adaptive layers puts the wordmark in the same frame the launcher |
| 415 | // icon uses. |
| 416 | image: variant.assets.androidSplashIcon, |
| 417 | imageWidth: 288, |
| 418 | dark: { image: variant.assets.androidSplashIcon }, |
| 419 | }, |
| 420 | }, |
| 421 | ], |
| 422 | [ |
| 423 | "expo-build-properties", |
| 424 | { |
| 425 | android: { |
| 426 | // Keep the supported floor explicit and covered by native notification tests. |
| 427 | minSdkVersion: 24, |
| 428 | // kotlinx-io uses Kotlin 2.3's return-value checker annotation, while |
| 429 | // SDK 58 builds with Kotlin 2.2. It has no runtime behavior. |
| 430 | // |
| 431 | // WorkManager 2.9 keeps InputMerger classes but not their constructors, |
| 432 | // and R8 full mode no longer keeps a default constructor implicitly. |
| 433 | // Without it no work request can start, so the Glance session behind |
| 434 | // the widget never renders and it stays on "Loading widget". WorkManager |
| 435 | // 2.10 ships this rule itself; drop it once the resolved version gets there. |
| 436 | extraProguardRules: [ |
| 437 | "-dontwarn kotlin.MustUseReturnValues", |
| 438 | "-keep class * extends androidx.work.InputMerger { <init>(); }", |
| 439 | ].join("\n"), |
| 440 | }, |
| 441 | ios: { |
| 442 | deploymentTarget: "18.0", |
| 443 | // AppCheckCore 11.3+ includes Swift and needs module maps for these Objective-C dependencies. |
| 444 | extraPods: [ |
| 445 | { name: "GoogleUtilities", modular_headers: true }, |
| 446 | { name: "RecaptchaInterop", modular_headers: true }, |
| 447 | ], |
| 448 | }, |
| 449 | }, |
| 450 | ], |
| 451 | "./plugins/withIosCocoaPodsUuidCache.cjs", |
| 452 | // Only the accelerometer is used (device viewer shake). Compile out the |
| 453 | // pedometer so iOS needs no motion purpose string. |
| 454 | ["expo-sensors", { motionPermission: false }], |
| 455 | // Must be listed BEFORE expo-widgets: same-type mods run last-registered- |
| 456 | // first, so registering earlier makes this plugin's mods run AFTER |
| 457 | // expo-widgets' — its dangerous mod wipes ios/ExpoWidgetsTarget/ (which |
| 458 | // would delete the asset catalog) and its xcodeproj mod creates the widget |
| 459 | // target (which must exist before the compile phase can be attached). |
| 460 | ...(!isIosPersonalTeamBuild ? ["./plugins/withWidgetLogoAsset.cjs", widgetsPlugin] : []), |
| 461 | "./plugins/withAndroidCleartextTraffic.cjs", |
| 462 | "./plugins/withAndroidGradleHeap.cjs", |
| 463 | "./plugins/withAndroidInputBackground.cjs", |
| 464 | "./plugins/withAndroidModernPopupMenu.cjs", |
| 465 | "./plugins/withAndroidModernAlertDialog.cjs", |
| 466 | "./plugins/withAndroidPredictiveBackCompat.cjs", |
| 467 | "./plugins/withAndroidTabletOrientation.cjs", |
| 468 | ], |
| 469 | extra: { |
| 470 | appVariant: APP_VARIANT, |
| 471 | iosPersonalTeamBuild: isIosPersonalTeamBuild, |
| 472 | relay: { |
| 473 | url: repoEnv.T3CODE_RELAY_URL ?? null, |
| 474 | }, |
| 475 | clerk: { |
| 476 | publishableKey: repoEnv.EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY ?? null, |
| 477 | jwtTemplate: repoEnv.EXPO_PUBLIC_CLERK_JWT_TEMPLATE ?? null, |
| 478 | }, |
| 479 | // Native Google sign-in credentials. @clerk/expo reads these from `extra` |
| 480 | // under their exact env-var names (not nested), and its config plugin reads |
| 481 | // the iOS URL scheme at prebuild to register it in Info.plist. |
| 482 | // Unset values must be omitted (not null): the public manifest serializes |
| 483 | // null to {}, which is truthy and would defeat Clerk's fallback checks. |
| 484 | EXPO_PUBLIC_CLERK_GOOGLE_WEB_CLIENT_ID: repoEnv.EXPO_PUBLIC_CLERK_GOOGLE_WEB_CLIENT_ID, |
| 485 | EXPO_PUBLIC_CLERK_GOOGLE_IOS_CLIENT_ID: repoEnv.EXPO_PUBLIC_CLERK_GOOGLE_IOS_CLIENT_ID, |
| 486 | EXPO_PUBLIC_CLERK_GOOGLE_ANDROID_CLIENT_ID: repoEnv.EXPO_PUBLIC_CLERK_GOOGLE_ANDROID_CLIENT_ID, |
| 487 | EXPO_PUBLIC_CLERK_GOOGLE_IOS_URL_SCHEME: repoEnv.EXPO_PUBLIC_CLERK_GOOGLE_IOS_URL_SCHEME, |
| 488 | eas: { |
| 489 | projectId: "d763fcb8-d37c-41ea-a773-b54a0ab4a454", |
| 490 | }, |
| 491 | }, |
| 492 | owner: "pingdotgg", |
| 493 | }; |
| 494 | |
| 495 | export default config; |