mirror of
https://github.com/suitenumerique/messages.git
synced 2026-08-17 21:25:41 +02:00
🔧(mobile) make the app identity and auth scheme env-driven
A staging and a production build must be installable side by side on one device, and two apps claiming the same OIDC deep-link scheme would make Android ask the user which one receives the login callback, mid-flow. The app id, the displayed name and the callback scheme therefore become per-environment (MOBILE_APP_ID / MOBILE_APP_NAME / MOBILE_AUTH_SCHEME).
This commit is contained in:
@@ -758,8 +758,26 @@ mobile-android-reverse: ## (host) map device ports to the dev stack via adb reve
|
||||
@$(foreach port,$(ANDROID_REVERSE_PORTS),adb reverse tcp:$(port) tcp:$(port);)
|
||||
.PHONY: mobile-android-reverse
|
||||
|
||||
# Read a MOBILE_* value from the frontend env files the way the container does
|
||||
# (frontend.local overriding frontend.defaults, last definition wins). Gradle
|
||||
# runs on the host and would otherwise read a *different* MOBILE_APP_ID than the
|
||||
# `cap sync` that produced the bundle — a divergence a store upload freezes
|
||||
# forever. A gradle guard cross-checks the two (android/app/build.gradle).
|
||||
# Surrounding double quotes are stripped the way compose's dotenv parser does,
|
||||
# so a quoted value does not reach gradle with its quotes and trip the appId
|
||||
# cross-check with an unreadable message.
|
||||
mobile_env = $(shell sed -n 's/^$(1)=//p' deploy/env/frontend.defaults deploy/env/frontend.local 2>/dev/null | tail -n1 | sed 's/^"//;s/"$$//')
|
||||
|
||||
# The same env the container used for `cap sync` must reach the host gradle
|
||||
# build, debug included: a MOBILE_AUTH_SCHEME set only on the JS side would ship
|
||||
# a manifest declaring the old scheme, and the OIDC callback would never come
|
||||
# back — the login opens, and nothing returns.
|
||||
mobile-android-run: mobile-build ## (host) build+install the debug APK on a device then adb reverse
|
||||
@cd src/frontend/android && ./gradlew assembleDebug
|
||||
@cd src/frontend/android && \
|
||||
MOBILE_APP_ID="$(call mobile_env,MOBILE_APP_ID)" \
|
||||
MOBILE_APP_NAME="$(call mobile_env,MOBILE_APP_NAME)" \
|
||||
MOBILE_AUTH_SCHEME="$(call mobile_env,MOBILE_AUTH_SCHEME)" \
|
||||
./gradlew assembleDebug
|
||||
@adb install -r $(ANDROID_DEBUG_APK)
|
||||
@$(MAKE) mobile-android-reverse
|
||||
.PHONY: mobile-android-run
|
||||
|
||||
Vendored
+1
@@ -20,6 +20,7 @@ NEXT_PUBLIC_SENTRY_ENVIRONMENT=
|
||||
## An organisation publishing to the stores overrides it — in BOTH contexts — with
|
||||
## its own signed bundle id, which never lands in this open-source repo.
|
||||
MOBILE_APP_ID=local.suitenumerique.messages
|
||||
MOBILE_APP_NAME=
|
||||
|
||||
## Mobile hot reload (Capacitor). Baked as the WebView's server.url at
|
||||
## `cap sync` (capacitor.config.ts): the app loads the JS straight from the
|
||||
|
||||
+4
-2
@@ -274,11 +274,13 @@ _Those settings are deprecated and will be removed in the future._
|
||||
|
||||
| Variable | Default | Description | Required |
|
||||
|----------|---------|-------------|----------|
|
||||
| `MOBILE_APP_ID` | `local.suitenumerique.messages` | Store/OS bundle identifier of the native app. The repo ships a neutral placeholder; an organisation publishing to the App Store / Play Store overrides it with its own signed id. Read by `cap sync` (container) **and** the native builds — gradle `applicationId`, iOS `PRODUCT_BUNDLE_IDENTIFIER` — so it must be exported in **both** the container env and the host/CI env. Independent of the auth callback scheme (`stmessages`). | Optional |
|
||||
| `MOBILE_APP_ID` | `local.suitenumerique.messages` | Store/OS bundle identifier of the native app. The repo ships a neutral placeholder; an organisation publishing to the App Store / Play Store overrides it with its own signed id. Read by `cap sync` (container) **and** the native builds: gradle `applicationId` reads the host/CI env (`make mobile-android-run` exports it for you), while Xcode reads `ios/App/generated.xcconfig`, written by `make mobile-build` from the container env — no export can reach an IDE build. Independent of the auth callback scheme (`MOBILE_AUTH_SCHEME`). | Optional |
|
||||
| `MOBILE_APP_NAME` | `ST Messages` | The application name displayed on the device. The repo ships a neutral placeholder; an organisation publishing to the App Store / Play Store overrides it with its own name. Read by `cap sync` (container) **and** the native builds, through the same two channels as `MOBILE_APP_ID`: gradle `resValue app_name` (host/CI env, exported by `make mobile-android-run`) and the iOS `PRODUCT_DISPLAY_NAME` setting in `ios/App/generated.xcconfig` (written by `make mobile-build`). | Optional |
|
||||
| `MOBILE_AUTH_SCHEME` | `stmessages` | Deep-link scheme ending the mobile OIDC flow. Give each environment its own value so a staging and a production build can be installed side by side — sharing it makes Android ask the user which app should receive the login callback, mid-flow. Read by Vite for the JS side **and** by the native builds: Android `manifestPlaceholders` reads the host/CI env (`make mobile-android-run` exports it for you), iOS reads the `AUTH_CALLBACK_SCHEME` setting from `ios/App/generated.xcconfig` (written by `make mobile-build`). Must be listed in the backend's `MOBILE_AUTH_CALLBACK_SCHEMES`. | Optional |
|
||||
| `MOBILE_DEV_SERVER_URL` | `http://localhost:8900` (dev env, `frontend.defaults`) | **Dev only.** URL of the Vite dev server baked as Capacitor `server.url` at `cap sync` (`capacitor.config.ts`): the WebView then loads the app from Vite with hot reload instead of the embedded bundle. To disable (embedded bundle / OTA testing), set it **empty** in `frontend.local` and rerun `make mobile-build`. Must never be set for a release build — a gradle guard fails Android release builds carrying it. See [mobile.md](./mobile.md#hot-reload-on-by-default-in-dev) | Optional |
|
||||
| `MOBILE_ALLOW_CLEARTEXT_FOR_DEV` | `1` (dev env, `frontend.defaults`) | **Dev only.** Baked as Capacitor `server.cleartext` at `cap sync` (`capacitor.config.ts`), i.e. `android:usesCleartextTraffic` in the Android manifest: allows plain HTTP for the whole app — the WebView reaching the Vite dev server and the native fetch/OTA layer reaching the `http://localhost:8901` backend and the RustFS OTA bucket (needed even with hot reload disabled). Must never be set for a release build — the manifest then stays cleartext-free. iOS equivalent: `NSAllowsLocalNetworking` (`Info.plist`, manual). | Optional |
|
||||
|
||||
> **Note**: overriding `MOBILE_APP_ID` only changes the app identity; it does **not** touch the OIDC deep-link scheme (`stmessages`), which is fixed and declared in the iOS `Info.plist` (`CFBundleURLTypes`) and the Android manifest.
|
||||
> **Note**: overriding `MOBILE_APP_ID` only changes the app identity; the OIDC deep-link scheme is a separate knob (`MOBILE_AUTH_SCHEME`), declared in the iOS `Info.plist` (`CFBundleURLTypes`) and the Android manifest. Changing one without the other is valid — but two builds installed together need **both** to differ.
|
||||
|
||||
### Mobile App Authentication (Capacitor)
|
||||
|
||||
|
||||
@@ -53,16 +53,19 @@ captures/
|
||||
.idea/navEditor.xml
|
||||
|
||||
# Keystore files
|
||||
# Uncomment the following lines if you do not want to check your keystore files in.
|
||||
#*.jks
|
||||
#*.keystore
|
||||
# The Play upload key and its passwords are per-instance secrets: like
|
||||
# MOBILE_APP_ID and google-services.json, the publishing organisation supplies
|
||||
# its own and none of it ever belongs in this open-source repo.
|
||||
*.jks
|
||||
*.keystore
|
||||
keystore.properties
|
||||
|
||||
# External native build folder generated in Android Studio 2.2 and later
|
||||
.externalNativeBuild
|
||||
.cxx/
|
||||
|
||||
# Google Services (e.g. APIs or Firebase)
|
||||
# google-services.json
|
||||
google-services.json*
|
||||
|
||||
# Freeline
|
||||
freeline.py
|
||||
|
||||
@@ -13,10 +13,25 @@ android {
|
||||
// via MOBILE_APP_ID (kept out of this open-source repo). It may differ
|
||||
// from the namespace above.
|
||||
applicationId System.getenv("MOBILE_APP_ID") ?: "local.suitenumerique.messages"
|
||||
// Displayed app name, same per-instance override pattern as the
|
||||
// applicationId above. Defined as resValues (not values/strings.xml,
|
||||
// which would clash with them) so MOBILE_APP_NAME can drive them at
|
||||
// build time; the manifest keeps pointing at @string/app_name.
|
||||
def appDisplayName = System.getenv("MOBILE_APP_NAME") ?: "ST Messages"
|
||||
resValue "string", "app_name", appDisplayName
|
||||
resValue "string", "title_activity_main", appDisplayName
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
// OIDC callback scheme declared by the manifest's intent-filter. Per
|
||||
// environment (like applicationId) so a staging and a production build
|
||||
// can coexist on one device: sharing it would make Android prompt the
|
||||
// user to pick an app mid-login. The default must match auth.ts and the
|
||||
// iOS AUTH_CALLBACK_SCHEME build setting (sso-invariants.test.ts).
|
||||
manifestPlaceholders = [
|
||||
authCallbackScheme: System.getenv("MOBILE_AUTH_SCHEME") ?: "stmessages"
|
||||
]
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
aaptOptions {
|
||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||
@@ -47,9 +62,10 @@ tasks.configureEach { task ->
|
||||
if (task.name == 'preReleaseBuild') {
|
||||
task.doFirst {
|
||||
def capConfig = file('src/main/assets/capacitor.config.json')
|
||||
def server = capConfig.exists()
|
||||
? new groovy.json.JsonSlurper().parse(capConfig)?.server
|
||||
def parsed = capConfig.exists()
|
||||
? new groovy.json.JsonSlurper().parse(capConfig)
|
||||
: null
|
||||
def server = parsed?.server
|
||||
if (server?.url) {
|
||||
throw new GradleException(
|
||||
"capacitor.config.json contains a dev server.url (hot reload). " +
|
||||
@@ -61,6 +77,33 @@ tasks.configureEach { task ->
|
||||
"(MOBILE_ALLOW_CLEARTEXT_FOR_DEV). " +
|
||||
"Rerun a bare 'make mobile-build' before building a release.")
|
||||
}
|
||||
// MOBILE_APP_ID is read twice from two different environments: by
|
||||
// `cap sync` in the container (the appId written to the config read
|
||||
// here) and by gradle on the host (applicationId above). Forgetting
|
||||
// the host half is silent — and a store upload freezes the id for
|
||||
// the lifetime of the app.
|
||||
def syncedAppId = parsed?.appId
|
||||
def builtAppId = android.defaultConfig.applicationId
|
||||
if (syncedAppId && syncedAppId != builtAppId) {
|
||||
throw new GradleException(
|
||||
"applicationId '${builtAppId}' does not match the appId " +
|
||||
"'${syncedAppId}' synced into capacitor.config.json. " +
|
||||
"Export the same MOBILE_APP_ID for the host gradle build " +
|
||||
"as for 'make mobile-build'.")
|
||||
}
|
||||
// Same double-read as MOBILE_APP_ID: the displayed name (resValue
|
||||
// above) comes from the host env, the synced appName from the
|
||||
// container env. A divergence is recoverable (unlike the appId) but
|
||||
// ships a wrong name silently, so fail it the same way.
|
||||
def syncedAppName = parsed?.appName
|
||||
def builtAppName = System.getenv("MOBILE_APP_NAME") ?: "ST Messages"
|
||||
if (syncedAppName && syncedAppName != builtAppName) {
|
||||
throw new GradleException(
|
||||
"app name '${builtAppName}' does not match the appName " +
|
||||
"'${syncedAppName}' synced into capacitor.config.json. " +
|
||||
"Export the same MOBILE_APP_NAME for the host gradle build " +
|
||||
"as for 'make mobile-build'.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,15 +21,18 @@
|
||||
|
||||
<!--
|
||||
Deep-link scheme ending the mobile OIDC flow: routes the
|
||||
stmessages://auth?token=… redirect from the Custom Tab back to
|
||||
<scheme>://auth?token=… redirect from the Custom Tab back to
|
||||
the app (caught by App.addListener("appUrlOpen")).
|
||||
Substituted from MOBILE_AUTH_SCHEME by the manifestPlaceholder
|
||||
in build.gradle, so a staging build declares its own scheme and
|
||||
does not fight the production app for the callback.
|
||||
Source of truth: src/features/native/auth.ts (AUTH_CALLBACK_SCHEME).
|
||||
-->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="stmessages" />
|
||||
<data android:scheme="${authCallbackScheme}" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<resources>
|
||||
<string name="app_name">Messages</string>
|
||||
<string name="title_activity_main">Messages</string>
|
||||
<!-- app_name / title_activity_main are resValues driven by MOBILE_APP_NAME
|
||||
(see app/build.gradle); defining them here too would clash. -->
|
||||
<string name="package_name">local.suitenumerique.messages</string>
|
||||
<string name="custom_url_scheme">local.suitenumerique.messages</string>
|
||||
|
||||
|
||||
@@ -46,12 +46,15 @@ const otaBuildId = process.env.MOBILE_OTA_BUILD_ID;
|
||||
const devServerUrl = process.env.MOBILE_DEV_SERVER_URL;
|
||||
|
||||
const config: CapacitorConfig = {
|
||||
// Build-time app identity. The repo ships a neutral placeholder; an
|
||||
// organisation publishing to the stores overrides it with its own signed
|
||||
// bundle id via the MOBILE_APP_ID env var (read here by `cap sync`, and by
|
||||
// the native builds — see android/app/build.gradle and the iOS pbxproj).
|
||||
appId: process.env.MOBILE_APP_ID ?? "local.suitenumerique.messages",
|
||||
appName: "Messages",
|
||||
// Build-time app identity. The repo ships neutral placeholders; an
|
||||
// organisation publishing to the stores overrides them via the MOBILE_APP_ID
|
||||
// (signed bundle id) and MOBILE_APP_NAME (displayed name) env vars — read
|
||||
// here by `cap sync`, and by the native builds: gradle resValue/applicationId
|
||||
// on Android (android/app/build.gradle), the generated xcconfig on iOS
|
||||
// (scripts/generate-ios-xcconfig.mjs). `||` not `??`: the env files ship the
|
||||
// vars empty, and an empty string must fall back like an unset one.
|
||||
appId: process.env.MOBILE_APP_ID || "local.suitenumerique.messages",
|
||||
appName: process.env.MOBILE_APP_NAME || "ST Messages",
|
||||
webDir: "dist",
|
||||
server: {
|
||||
// Dev only: `cap sync` turns this into android:usesCleartextTraffic in the
|
||||
|
||||
@@ -11,3 +11,4 @@ capacitor-cordova-ios-plugins
|
||||
# Generated Config files
|
||||
App/App/capacitor.config.json
|
||||
App/App/config.xml
|
||||
App/generated.xcconfig
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
7A11AA0100000000000000E1 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
7A11AA0100000000000000E2 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
958DCC722DB07C7200EA8C5F /* debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = debug.xcconfig; path = ../debug.xcconfig; sourceTree = SOURCE_ROOT; };
|
||||
7A11AA0100000000000000F1 /* generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = generated.xcconfig; sourceTree = SOURCE_ROOT; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@@ -54,6 +55,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
958DCC722DB07C7200EA8C5F /* debug.xcconfig */,
|
||||
7A11AA0100000000000000F1 /* generated.xcconfig */,
|
||||
504EC3061FED79650016851F /* App */,
|
||||
504EC3051FED79650016851F /* Products */,
|
||||
);
|
||||
@@ -97,6 +99,7 @@
|
||||
504EC3011FED79650016851F /* Frameworks */,
|
||||
504EC3021FED79650016851F /* Resources */,
|
||||
7A11AA0100000000000000C1 /* Strip dev ATS exception */,
|
||||
7A11AA0100000000000000C2 /* Check synced Capacitor identity */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
@@ -187,6 +190,28 @@
|
||||
shellScript = "# NSAllowsLocalNetworking in App/Info.plist exists only so dev builds can\n# reach the plain-HTTP backend on localhost. Delete the whole ATS dict from\n# the built product in every non-Debug configuration so the exception can\n# never ship (Archive always builds Release).\nif [ \"${CONFIGURATION}\" != \"Debug\" ]; then\n PLIST=\"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}\"\n if /usr/libexec/PlistBuddy -c \"Print :NSAppTransportSecurity\" \"${PLIST}\" >/dev/null 2>&1; then\n /usr/libexec/PlistBuddy -c \"Delete :NSAppTransportSecurity\" \"${PLIST}\"\n fi\nfi\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
7A11AA0100000000000000C2 /* Check synced Capacitor identity */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
"$(TARGET_BUILD_DIR)/$(INFOPLIST_PATH)",
|
||||
"$(SRCROOT)/App/capacitor.config.json",
|
||||
);
|
||||
name = "Check synced Capacitor identity";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "# `make mobile-build` writes generated.xcconfig and capacitor.config.json in\n# one run from one env; the identity Xcode compiled must match what was\n# synced. A stale or hand-edited value would otherwise ship silently — and\n# the first store upload freezes the bundle id forever. Debug builds skip the\n# check (Android equivalent: the preReleaseBuild guard in app/build.gradle).\nif [ \"${CONFIGURATION}\" != \"Debug\" ]; then\n CONFIG_JSON=\"${SRCROOT}/App/capacitor.config.json\"\n PLIST=\"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}\"\n SYNCED_ID=$(/usr/bin/plutil -extract appId raw -o - \"${CONFIG_JSON}\")\n SYNCED_NAME=$(/usr/bin/plutil -extract appName raw -o - \"${CONFIG_JSON}\")\n BUILT_ID=$(/usr/libexec/PlistBuddy -c \"Print :CFBundleIdentifier\" \"${PLIST}\")\n BUILT_NAME=$(/usr/libexec/PlistBuddy -c \"Print :CFBundleDisplayName\" \"${PLIST}\")\n if [ \"${SYNCED_ID}\" != \"${BUILT_ID}\" ] || [ \"${SYNCED_NAME}\" != \"${BUILT_NAME}\" ]; then\n echo \"error: built identity '${BUILT_ID}' / '${BUILT_NAME}' does not match the synced capacitor.config.json '${SYNCED_ID}' / '${SYNCED_NAME}'. Rerun 'make mobile-build' so generated.xcconfig and the synced config agree.\" >&2\n exit 1\n fi\nfi\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
@@ -233,7 +258,7 @@
|
||||
/* Begin XCBuildConfiguration section */
|
||||
504EC3141FED79650016851F /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 958DCC722DB07C7200EA8C5F /* debug.xcconfig */;
|
||||
baseConfigurationReference = 7A11AA0100000000000000F1 /* generated.xcconfig */;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
@@ -291,6 +316,7 @@
|
||||
};
|
||||
504EC3151FED79650016851F /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 7A11AA0100000000000000F1 /* generated.xcconfig */;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
@@ -356,8 +382,7 @@
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
|
||||
MOBILE_APP_ID = "local.suitenumerique.messages";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(MOBILE_APP_ID)";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(MOBILE_APP_ID:default=local.suitenumerique.messages)";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_VERSION = 5.0;
|
||||
@@ -379,8 +404,7 @@
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 1.0;
|
||||
MOBILE_APP_ID = "local.suitenumerique.messages";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(MOBILE_APP_ID)";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = "$(MOBILE_APP_ID:default=local.suitenumerique.messages)";
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
|
||||
SWIFT_VERSION = 5.0;
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>$(PRODUCT_DISPLAY_NAME:default=Messages)</string>
|
||||
<string>$(PRODUCT_DISPLAY_NAME:default=ST Messages)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
@@ -36,7 +36,7 @@
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER).auth</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>stmessages</string>
|
||||
<string>$(AUTH_CALLBACK_SCHEME:default=stmessages)</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"ts:check": "tsc --noEmit",
|
||||
"i18n:extract": "i18next-cli extract",
|
||||
"analyze": "ANALYZE=1 vite build && node ./scripts/print-bundle-stats.mjs",
|
||||
"mobile:build": "npm run build && npx cap sync",
|
||||
"mobile:build": "npm run build && npx cap sync && node scripts/generate-ios-xcconfig.mjs",
|
||||
"mobile:ota:bucket": "node scripts/create-ota-bucket.mjs",
|
||||
"mobile:ota:keygen": "node scripts/generate-ota-keys.mjs",
|
||||
"mobile:ota:publish": "node scripts/publish-ota.mjs",
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
// Write ios/App/generated.xcconfig from the MOBILE_* env of the container
|
||||
// build, alongside `cap sync` (npm run mobile:build). Xcode builds run from the
|
||||
// IDE on the host, where an exported shell variable never lands — this file is
|
||||
// how the same env that produced capacitor.config.json reaches the iOS build
|
||||
// settings (bundle id, display name, auth scheme). Android needs no such file:
|
||||
// gradle reads the env directly (see android/app/build.gradle and the
|
||||
// mobile-android-* Makefile targets).
|
||||
//
|
||||
// The file is gitignored (per-instance identity, like the synced config); the
|
||||
// fallbacks below must match the ones at each point of use — gradle, auth.ts
|
||||
// and the Info.plist `:default=` operators (pinned by sso-invariants.test.ts).
|
||||
//
|
||||
// Usage: node scripts/generate-ios-xcconfig.mjs
|
||||
import { writeFileSync } from "node:fs";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const settings = {
|
||||
MOBILE_APP_ID: process.env.MOBILE_APP_ID || "local.suitenumerique.messages",
|
||||
PRODUCT_DISPLAY_NAME: process.env.MOBILE_APP_NAME || "ST Messages",
|
||||
AUTH_CALLBACK_SCHEME: process.env.MOBILE_AUTH_SCHEME || "stmessages",
|
||||
};
|
||||
|
||||
// A newline or a "//" would truncate the setting (xcconfig comment/line
|
||||
// syntax) and silently ship the placeholder identity instead of failing.
|
||||
for (const [key, value] of Object.entries(settings)) {
|
||||
if (/[\n\r]|\/\//.test(value)) {
|
||||
throw new Error(`${key} value ${JSON.stringify(value)} is not a valid single-line xcconfig value`);
|
||||
}
|
||||
}
|
||||
|
||||
const target = resolve(
|
||||
dirname(fileURLToPath(import.meta.url)),
|
||||
"../ios/App/generated.xcconfig",
|
||||
);
|
||||
|
||||
writeFileSync(
|
||||
target,
|
||||
[
|
||||
"// Generated by scripts/generate-ios-xcconfig.mjs (make mobile-build) — do not edit.",
|
||||
...Object.entries(settings).map(([key, value]) => `${key} = ${value}`),
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
|
||||
process.stderr.write(`Wrote ${target}\n`);
|
||||
Vendored
+4
@@ -39,6 +39,10 @@ interface ImportMetaEnv {
|
||||
// (capacitor.config.ts). Exposed so ota.ts can refuse a server-provided
|
||||
// manifest URL on a build that embeds no key.
|
||||
readonly MOBILE_OTA_SIGNING_PUBLIC_KEY_B64?: string;
|
||||
// OIDC deep-link scheme, per environment so two builds can coexist on a
|
||||
// device. Also declared natively (Android manifestPlaceholder, iOS build
|
||||
// setting) from the same variable — see auth.ts.
|
||||
readonly MOBILE_AUTH_SCHEME?: string;
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
|
||||
@@ -89,10 +89,13 @@ export default defineConfig({
|
||||
// hot reload session (see capacitor.config.ts) and skip the OTA check there.
|
||||
// MOBILE_OTA_SIGNING_PUBLIC_KEY_B64 (public key, safe to inline) lets ota.ts
|
||||
// refuse a server-provided manifest URL on a build that can't verify bundles.
|
||||
// MOBILE_AUTH_SCHEME must match the scheme the native projects declare, so
|
||||
// auth.ts builds its callback URL with the one the OS will actually route.
|
||||
envPrefix: [
|
||||
'NEXT_PUBLIC_',
|
||||
'MOBILE_DEV_SERVER_URL',
|
||||
'MOBILE_OTA_SIGNING_PUBLIC_KEY_B64',
|
||||
'MOBILE_AUTH_SCHEME',
|
||||
],
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
|
||||
Reference in New Issue
Block a user