423 lines · 15.8 KB · TypeScript
RawDownload
import type { ExpoConfig } from "expo/config"; import { BRAND_ASSET_PATHS } from "../../scripts/lib/brand-assets.ts";import { loadRepoEnv } from "../../scripts/lib/public-config.ts"; type AppVariant = "development" | "preview" | "production"; const repoEnv = loadRepoEnv();Object.assign(process.env, repoEnv); const APP_VARIANT = resolveAppVariant(repoEnv.APP_VARIANT);const isIosPersonalTeamBuild = repoEnv.T3CODE_IOS_PERSONAL_TEAM === "1";const runtimeVersionPolicy =  process.env.MOBILE_VERSION_POLICY ??  (APP_VARIANT === "development" ? "appVersion" : "fingerprint"); const personalTeamBundleIdentifier = repoEnv.T3CODE_IOS_PERSONAL_TEAM_BUNDLE_ID?.trim();const IOS_BUNDLE_IDENTIFIER_PATTERN = /^[A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)+$/; const fromRepoRoot = (relativePath: string) => `../../${relativePath}`;// Android layers are rendered by scripts/export-android-icons.ts from the Icon Composer sources.// The wordmark sits inside the adaptive safe zone; the variant artwork is a full-bleed background.const androidAdaptiveForeground = "./assets/android-icon-foreground.png"; if (  isIosPersonalTeamBuild &&  (!personalTeamBundleIdentifier ||    !IOS_BUNDLE_IDENTIFIER_PATTERN.test(personalTeamBundleIdentifier))) {  throw new Error(    "T3CODE_IOS_PERSONAL_TEAM_BUNDLE_ID must be a reverse-DNS identifier such as com.example.t3code when T3CODE_IOS_PERSONAL_TEAM=1.",  );} const DEVELOPMENT_ASSETS = {  appIcon: fromRepoRoot(BRAND_ASSET_PATHS.developmentIosIconPng),  iosIcon: fromRepoRoot(BRAND_ASSET_PATHS.developmentIconComposerProject),  splashIcon: fromRepoRoot(BRAND_ASSET_PATHS.developmentIosIconPng),  androidAdaptiveForeground,  androidAdaptiveBackgroundColor: "#347FF8",  androidAdaptiveBackgroundImage: "./assets/android-icon-background-dev.png",  androidSplashIcon: "./assets/android-splash-icon-dev.png",  androidMonochromeIcon: "./assets/android-icon-mark.png",  androidNotificationIcon: "./assets/android-notification-icon.png",  androidNotificationColor: "#00639B",} as const; const PREVIEW_ASSETS = {  appIcon: fromRepoRoot(BRAND_ASSET_PATHS.nightlyIosIconPng),  iosIcon: fromRepoRoot(BRAND_ASSET_PATHS.nightlyIconComposerProject),  splashIcon: fromRepoRoot(BRAND_ASSET_PATHS.nightlyIosIconPng),  androidAdaptiveForeground,  androidAdaptiveBackgroundColor: "#111533",  androidAdaptiveBackgroundImage: "./assets/android-icon-background-nightly.png",  androidSplashIcon: "./assets/android-splash-icon-nightly.png",  androidMonochromeIcon: "./assets/android-icon-mark.png",  androidNotificationIcon: "./assets/android-notification-icon.png",  androidNotificationColor: "#7565C7",} as const; const RELEASE_ASSETS = {  appIcon: fromRepoRoot(BRAND_ASSET_PATHS.productionIosIconPng),  iosIcon: fromRepoRoot(BRAND_ASSET_PATHS.productionIconComposerProject),  splashIcon: fromRepoRoot(BRAND_ASSET_PATHS.productionIosIconPng),  androidAdaptiveForeground,  androidAdaptiveBackgroundColor: "#000000",  androidAdaptiveBackgroundImage: undefined,  androidSplashIcon: "./assets/android-splash-icon-prod.png",  androidMonochromeIcon: "./assets/android-icon-mark.png",  androidNotificationIcon: "./assets/android-notification-icon.png",  androidNotificationColor: "#FFFFFF",} as const; const VARIANT_CONFIG = {  development: {    appName: "T3 Code Dev",    scheme: "t3code-dev",    iosBundleIdentifier: "com.t3tools.t3code.dev",    androidPackage: "com.t3tools.t3code.dev",    relyingParty: "clerk.t3.codes",    assets: DEVELOPMENT_ASSETS,  },  preview: {    appName: "T3 Code Preview",    scheme: "t3code-preview",    iosBundleIdentifier: "com.t3tools.t3code.preview",    androidPackage: "com.t3tools.t3code.preview",    relyingParty: "clerk.t3.codes",    assets: PREVIEW_ASSETS,  },  production: {    appName: "T3 Code",    scheme: "t3code",    iosBundleIdentifier: "com.t3tools.t3code",    androidPackage: "com.t3tools.t3code",    relyingParty: "clerk.t3.codes",    assets: RELEASE_ASSETS,  },} as const; function resolveAppVariant(value: string | undefined): AppVariant {  switch (value) {    case "development":    case "preview":    case "production":      return value;    default:      return "production";  }} const variant = VARIANT_CONFIG[APP_VARIANT];const iosBundleIdentifier = isIosPersonalTeamBuild  ? personalTeamBundleIdentifier!  : variant.iosBundleIdentifier; const dmSansFonts = {  regular: "@expo-google-fonts/dm-sans/400Regular/DMSans_400Regular.ttf",  medium: "@expo-google-fonts/dm-sans/500Medium/DMSans_500Medium.ttf",  bold: "@expo-google-fonts/dm-sans/700Bold/DMSans_700Bold.ttf",} as const; const widgetsPlugin: NonNullable<ExpoConfig["plugins"]>[number] = [  "expo-widgets",  {    bundleIdentifier: `${iosBundleIdentifier}.widgets`,    groupIdentifier: `group.${iosBundleIdentifier}`,    enablePushNotifications: true,    // Agent activity can update many times an hour; without the    // frequent-updates entitlement iOS throttles the update budget sooner.    frequentUpdates: true,    widgets: [      {        name: "AgentActivity",        displayName: "Agent Activity",        description: "Shows the current state of active T3 Code agents.",        supportedFamilies: ["systemSmall", "systemMedium", "accessoryRectangular"],      },    ],  },]; const sharingPlugin: NonNullable<ExpoConfig["plugins"]>[number] = [  "expo-sharing",  {    ios: {      // Personal Teams cannot sign App Groups or extension targets. Keep the      // reduced-capability local build usable while release builds expose the      // real system share target.      enabled: !isIosPersonalTeamBuild,      extensionBundleIdentifier: `${iosBundleIdentifier}.sharing`,      appGroupId: `group.${iosBundleIdentifier}`,      activationRule: {        supportsText: true,        supportsWebUrlWithMaxCount: 1,        supportsImageWithMaxCount: 8,        supportsMovieWithMaxCount: 8,        supportsFileWithMaxCount: 8,      },    },    android: {      enabled: true,      singleShareMimeTypes: ["*/*"],      multipleShareMimeTypes: ["*/*"],    },  },]; // These aliases match the fonts' PostScript names on iOS. Register the same// names on Android so React Native and the native composer use one set of// family names without waiting for runtime font loading. const config: ExpoConfig = {  name: variant.appName,  slug: "t3-code",  platforms: ["ios", "android"],  scheme: variant.scheme,  version: "1.1.1",  runtimeVersion: {    // Development manifests resolve on every launch, so avoid fingerprint's    // expensive native-project calculation there. Preview and production stay    // fingerprinted so OTAs only reach binaries with matching native projects.    policy: runtimeVersionPolicy,  },  orientation: "portrait",  icon: variant.assets.appIcon,  userInterfaceStyle: "automatic",  updates: {    enabled: repoEnv.T3CODE_MOBILE_UPDATES_ENABLED !== "0",    url: "https://u.expo.dev/d763fcb8-d37c-41ea-a773-b54a0ab4a454",    checkAutomatically: "ON_LOAD",    fallbackToCacheTimeout: 0,  },  ios: {    icon: variant.assets.iosIcon,    supportsTablet: true,    // Multitasking-capable iPad apps cannot rotate programmatically, so the    // showcase capture build requires full screen (see infoPlist below).    requireFullScreen: process.env.T3_SHOWCASE_CAPTURE_BUILD === "1",    bundleIdentifier: iosBundleIdentifier,    // Pin code signing to the T3 Tools team so non-interactive `expo run:ios`    // does not fall back to a personal team (which cannot sign app groups,    // Sign in with Apple, or push notification entitlements).    appleTeamId: "ARK85ZXQ4Z",    associatedDomains: [      `applinks:${variant.relyingParty}`,      `webcredentials:${variant.relyingParty}`,    ],    entitlements: {      "keychain-access-groups": [`$(AppIdentifierPrefix)${variant.iosBundleIdentifier}`],    },    infoPlist: {      NSAppTransportSecurity: {        NSAllowsArbitraryLoads: true,      },      NSLocalNetworkUsageDescription:        "Allow T3 Code to connect to T3 Code servers on your local network or tailnet.",      NSPhotoLibraryAddUsageDescription: "Allow T3 Code to save images to your photo library.",      ITSAppUsesNonExemptEncryption: false,      // The App Store screenshot harness rotates the iPad interface from      // inside the app (CI denies osascript the Accessibility access that      // Simulator menu scripting needs), and iPadOS ignores programmatic      // orientation requests for multitasking-capable apps — so the capture      // build opts out of multitasking and declares landscape support.      ...(process.env.T3_SHOWCASE_CAPTURE_BUILD === "1"        ? {            "UISupportedInterfaceOrientations~ipad": [              "UIInterfaceOrientationPortrait",              "UIInterfaceOrientationPortraitUpsideDown",              "UIInterfaceOrientationLandscapeLeft",              "UIInterfaceOrientationLandscapeRight",            ],          }        : {}),    },  },  android: {    icon: variant.assets.appIcon,    package: variant.androidPackage,    ...(repoEnv.T3CODE_ANDROID_GOOGLE_SERVICES_FILE      ? { googleServicesFile: repoEnv.T3CODE_ANDROID_GOOGLE_SERVICES_FILE }      : {}),    adaptiveIcon: {      backgroundColor: variant.assets.androidAdaptiveBackgroundColor,      ...(variant.assets.androidAdaptiveBackgroundImage        ? { backgroundImage: variant.assets.androidAdaptiveBackgroundImage }        : {}),      foregroundImage: variant.assets.androidAdaptiveForeground,      monochromeImage: variant.assets.androidMonochromeIcon,    },    // Opts into OnBackInvokedCallback-based back dispatch (Android 13+).    // JS back handling survives it via react-native's Android 16 shim plus    // withAndroidPredictiveBackCompat on Android 13-15.    predictiveBackGestureEnabled: true,  },  web: {    favicon: variant.assets.appIcon,  },  plugins: [    "expo-asset",    [      "expo-font",      {        ios: {          fonts: [dmSansFonts.regular, dmSansFonts.medium, dmSansFonts.bold],        },        android: {          fonts: [            {              fontFamily: "DMSans-Regular",              fontDefinitions: [{ path: dmSansFonts.regular, weight: 400 }],            },            {              fontFamily: "DMSans-Medium",              fontDefinitions: [{ path: dmSansFonts.medium, weight: 500 }],            },            {              fontFamily: "DMSans-Bold",              fontDefinitions: [{ path: dmSansFonts.bold, weight: 700 }],            },          ],        },      },    ],    "expo-secure-store",    "expo-sqlite",    ...(isIosPersonalTeamBuild      ? [sharingPlugin]      : ["./plugins/withShareExtensionDisplayName.cjs", sharingPlugin]),    [      "expo-notifications",      {        icon: variant.assets.androidNotificationIcon,        color: variant.assets.androidNotificationColor,        mode: APP_VARIANT === "development" ? "development" : "production",      },    ],    // appleSignIn must be gated here: withoutIosPersonalTeamCapabilities.cjs runs before    // plugins earlier in this array, so it cannot strip the entitlement Clerk would add.    ["@clerk/expo", { theme: "./clerk-theme.json", appleSignIn: !isIosPersonalTeamBuild }],    "expo-web-browser",    [      "expo-quick-actions",      {        // Adaptive launcher-shortcut icon; referenced by resource name from        // the shortcut items set in src/features/shortcuts.        androidIcons: {          shortcut_icon: {            foregroundImage: variant.assets.androidAdaptiveForeground,            backgroundColor: variant.assets.androidAdaptiveBackgroundColor,            ...(variant.assets.androidAdaptiveBackgroundImage              ? { backgroundImage: variant.assets.androidAdaptiveBackgroundImage }              : {}),          },        },      },    ],    [      "expo-audio",      {        microphonePermission: "Allow T3 Code to use your microphone for voice input.",        recordAudioAndroid: false,        enableBackgroundPlayback: false,        enableBackgroundRecording: false,      },    ],    [      "expo-camera",      {        cameraPermission: "Allow T3 Code to access your camera so you can scan pairing QR codes.",        microphonePermission: false,        barcodeScannerEnabled: true,        recordAudioAndroid: false,      },    ],    ["expo-image-picker", { photosPermission: false, microphonePermission: false }],    [      "expo-splash-screen",      {        image: variant.assets.splashIcon,        resizeMode: "contain",        backgroundColor: "#ffffff",        imageWidth: 220,        dark: {          image: variant.assets.splashIcon,          backgroundColor: "#0a0a0a",        },        android: {          // Android 12+ masks the splash icon to a circle over the central two thirds of          // its 288dp canvas, so the iOS export's corners get cut. A full-canvas image of          // the composed adaptive layers puts the wordmark in the same frame the launcher          // icon uses.          image: variant.assets.androidSplashIcon,          imageWidth: 288,          dark: { image: variant.assets.androidSplashIcon },        },      },    ],    [      "expo-build-properties",      {        android: {          // Keep the supported floor explicit and covered by native notification tests.          minSdkVersion: 24,        },        ios: {          deploymentTarget: "18.0",          // AppCheckCore 11.3+ includes Swift and needs module maps for these Objective-C dependencies.          extraPods: [            { name: "GoogleUtilities", modular_headers: true },            { name: "RecaptchaInterop", modular_headers: true },          ],        },      },    ],    "./plugins/withIosCocoaPodsUuidCache.cjs",    // Must be listed BEFORE expo-widgets: same-type mods run last-registered-    // first, so registering earlier makes this plugin's mods run AFTER    // expo-widgets' — its dangerous mod wipes ios/ExpoWidgetsTarget/ (which    // would delete the asset catalog) and its xcodeproj mod creates the widget    // target (which must exist before the compile phase can be attached).    ...(!isIosPersonalTeamBuild ? ["./plugins/withWidgetLogoAsset.cjs", widgetsPlugin] : []),    "./plugins/withIosSceneLifecycle.cjs",    "./plugins/withAndroidCleartextTraffic.cjs",    "./plugins/withAndroidGradleHeap.cjs",    "./plugins/withAndroidModernPopupMenu.cjs",    "./plugins/withAndroidModernAlertDialog.cjs",    "./plugins/withAndroidPredictiveBackCompat.cjs",    "./plugins/withAndroidTabletOrientation.cjs",    ...(isIosPersonalTeamBuild ? ["./plugins/withoutIosPersonalTeamCapabilities.cjs"] : []),  ],  extra: {    appVariant: APP_VARIANT,    iosPersonalTeamBuild: isIosPersonalTeamBuild,    relay: {      url: repoEnv.T3CODE_RELAY_URL ?? null,    },    clerk: {      publishableKey: repoEnv.EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY ?? null,      jwtTemplate: repoEnv.EXPO_PUBLIC_CLERK_JWT_TEMPLATE ?? null,    },    // Native Google sign-in credentials. @clerk/expo reads these from `extra`    // under their exact env-var names (not nested), and its config plugin reads    // the iOS URL scheme at prebuild to register it in Info.plist.    // Unset values must be omitted (not null): the public manifest serializes    // null to {}, which is truthy and would defeat Clerk's fallback checks.    EXPO_PUBLIC_CLERK_GOOGLE_WEB_CLIENT_ID: repoEnv.EXPO_PUBLIC_CLERK_GOOGLE_WEB_CLIENT_ID,    EXPO_PUBLIC_CLERK_GOOGLE_IOS_CLIENT_ID: repoEnv.EXPO_PUBLIC_CLERK_GOOGLE_IOS_CLIENT_ID,    EXPO_PUBLIC_CLERK_GOOGLE_ANDROID_CLIENT_ID: repoEnv.EXPO_PUBLIC_CLERK_GOOGLE_ANDROID_CLIENT_ID,    EXPO_PUBLIC_CLERK_GOOGLE_IOS_URL_SCHEME: repoEnv.EXPO_PUBLIC_CLERK_GOOGLE_IOS_URL_SCHEME,    observability: {      tracesUrl: repoEnv.EXPO_PUBLIC_OTLP_TRACES_URL ?? "https://api.axiom.co/v1/traces",      tracesDataset: repoEnv.EXPO_PUBLIC_OTLP_TRACES_DATASET ?? null,      tracesToken: repoEnv.EXPO_PUBLIC_OTLP_TRACES_TOKEN ?? null,    },    eas: {      projectId: "d763fcb8-d37c-41ea-a773-b54a0ab4a454",    },  },  owner: "pingdotgg",}; export default config;