@livx.cc/appwrap 0.37.0 → 0.38.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/runtime/app/shell/config.ts +5 -0
- package/runtime/app/shell/custom-webview.ios.ts +4 -0
- package/src/cli.ts +67 -19
- package/src/config.ts +14 -0
- package/src/derive.ts +41 -0
- package/templates/ci/fastlane/Fastfile +83 -35
package/package.json
CHANGED
|
@@ -40,6 +40,11 @@ export const SHELL_CONFIG = {
|
|
|
40
40
|
* fights the app:// handler / remote-update detection). On by default; set false to opt out and keep
|
|
41
41
|
* the SW (e.g. for in-WebView web-push). See `serviceWorkerGuardJs`. */
|
|
42
42
|
neutralizeServiceWorker: true,
|
|
43
|
+
/** iOS App-Bound Domains (from `appwrap.json.appBoundDomains`). When non-empty the shell sets
|
|
44
|
+
* `WKWebViewConfiguration.limitsNavigationsToAppBoundDomains = true` — Apple's gate for running a
|
|
45
|
+
* service worker in the WKWebView (the same hosts are stamped to Info.plist `WKAppBoundDomains`).
|
|
46
|
+
* RESTRICTS the WebView to these domains, so only for single-origin apps. Empty = no restriction. */
|
|
47
|
+
appBoundDomains: [] as string[],
|
|
43
48
|
/** Open external-origin navigations (`<a>` to another origin, incl. `target="_blank"`, and
|
|
44
49
|
* `window.open(...)`) in the OS default browser (Safari / Chrome) instead of inside the shell
|
|
45
50
|
* WebView — regular-native-app behavior. Same-origin SPA navigation is untouched. Off by default.
|
|
@@ -180,6 +180,10 @@ export class CustomWebView extends WebView {
|
|
|
180
180
|
config.allowsPictureInPictureMediaPlayback = true;
|
|
181
181
|
config.mediaTypesRequiringUserActionForPlayback = 0; // WKAudiovisualMediaTypeNone
|
|
182
182
|
config.websiteDataStore = WKWebsiteDataStore.defaultDataStore();
|
|
183
|
+
// Apple's hard requirement for a service worker in a WKWebView: restrict navigation to the
|
|
184
|
+
// declared app-bound domains (also stamped to Info.plist WKAppBoundDomains). Only set when
|
|
185
|
+
// configured — otherwise the WebView stays unrestricted (default behavior).
|
|
186
|
+
if (SHELL_CONFIG.appBoundDomains?.length) config.limitsNavigationsToAppBoundDomains = true;
|
|
183
187
|
|
|
184
188
|
// Env hints + framework globals (backend origin) + a getUserMedia guard for UNDECLARED capabilities
|
|
185
189
|
// (must reject in JS before WebKit's native capture path — see mediaCaptureGuardJs) + native-feel
|
package/src/cli.ts
CHANGED
|
@@ -22,11 +22,13 @@ import type * as CapManifest from '../../../runtime/app/shell/capabilities.manif
|
|
|
22
22
|
import type { AppwrapConfig } from './config';
|
|
23
23
|
import {
|
|
24
24
|
androidScreenOrientation,
|
|
25
|
+
applyBuildNumberFlag,
|
|
25
26
|
iosOrientations,
|
|
26
27
|
mergeManifest,
|
|
27
28
|
resolveBuildNumber,
|
|
28
29
|
stampAndroidOrientation,
|
|
29
30
|
stampAndroidQueries,
|
|
31
|
+
stampAppBoundDomains,
|
|
30
32
|
stampPlistBackgroundTasks,
|
|
31
33
|
stampPlistOrientations,
|
|
32
34
|
stampPrivacyTracking,
|
|
@@ -409,6 +411,7 @@ export const SHELL_CONFIG = {
|
|
|
409
411
|
debugLog: ${JSON.stringify(cfg.debugLog ?? '*')},
|
|
410
412
|
devMenu: ${JSON.stringify(cfg.devMenu ?? true)},
|
|
411
413
|
neutralizeServiceWorker: ${JSON.stringify(cfg.neutralizeServiceWorker ?? true)},
|
|
414
|
+
appBoundDomains: ${JSON.stringify(cfg.appBoundDomains ?? [])},
|
|
412
415
|
openNewWindowsInBrowser: ${JSON.stringify(cfg.openNewWindowsInBrowser ?? false)},
|
|
413
416
|
pushIos: ${JSON.stringify(!!cfg.push?.enabled && cfg.push?.ios !== false)},
|
|
414
417
|
pushAndroid: ${JSON.stringify(!!cfg.push?.enabled && cfg.push?.android !== false)},
|
|
@@ -447,6 +450,11 @@ function stampIOSDisplayName(outDir: string, cfg: AppwrapConfig, req: NativeReqs
|
|
|
447
450
|
const bgActive = req.activeOptionalGroups.includes('backgroundTask');
|
|
448
451
|
src = stampPlistBackgroundTasks(src, bgActive ? cfg.backgroundTasks : undefined);
|
|
449
452
|
|
|
453
|
+
// iOS App-Bound Domains — gate for a WKWebView service worker (paired with
|
|
454
|
+
// limitsNavigationsToAppBoundDomains in the shell config). Idempotent both ways: empty/undefined
|
|
455
|
+
// strips the WKAppBoundDomains key, so it no-ops when the field is absent.
|
|
456
|
+
src = stampAppBoundDomains(src, cfg.appBoundDomains);
|
|
457
|
+
|
|
450
458
|
// Permission usage strings + URL scheme + export-compliance — idempotent: strip stamped block, re-add
|
|
451
459
|
src = src.replace(/\s*<!-- appwrap:begin -->[\s\S]*?<!-- appwrap:end -->/g, '');
|
|
452
460
|
const extras: string[] = [];
|
|
@@ -525,6 +533,20 @@ function stampTeamId(outDir: string, cfg: AppwrapConfig, ctx?: { cwd: string; co
|
|
|
525
533
|
writeFileSync(xcconfig, src);
|
|
526
534
|
}
|
|
527
535
|
|
|
536
|
+
/** Stamp TARGETED_DEVICE_FAMILY into build.xcconfig from cfg.targetedDevices. `'iphone'` → `1`
|
|
537
|
+
* (iPhone-only → UIDeviceFamily=[1], so the App Store doesn't require iPad screenshots);
|
|
538
|
+
* `'universal'`/unset → `1,2` (NativeScript's default). Idempotent: replaces any prior value. */
|
|
539
|
+
function stampDeviceFamily(outDir: string, cfg: AppwrapConfig): void {
|
|
540
|
+
const xcconfig = join(outDir, 'App_Resources/iOS/build.xcconfig');
|
|
541
|
+
if (!existsSync(xcconfig)) return;
|
|
542
|
+
const value = cfg.targetedDevices === 'iphone' ? '1' : '1,2';
|
|
543
|
+
let src = readFileSync(xcconfig, 'utf8');
|
|
544
|
+
src = /TARGETED_DEVICE_FAMILY\s*=/.test(src)
|
|
545
|
+
? src.replace(/TARGETED_DEVICE_FAMILY\s*=\s*[^;\n]*;?/, `TARGETED_DEVICE_FAMILY = ${value};`)
|
|
546
|
+
: src + `\nTARGETED_DEVICE_FAMILY = ${value};\n`;
|
|
547
|
+
writeFileSync(xcconfig, src);
|
|
548
|
+
}
|
|
549
|
+
|
|
528
550
|
/** Wire a StoreKit config file for LOCAL iOS IAP testing (no App Store Connect needed).
|
|
529
551
|
* NativeScript copies App_Resources/iOS/* into the generated project and adds it as a file
|
|
530
552
|
* reference — but it never points the scheme at it, so StoreKit has no catalog. We (1) drop the
|
|
@@ -1036,6 +1058,7 @@ function regenerateCore(cwd: string, outDir: string, cfg: AppwrapConfig, opts: {
|
|
|
1036
1058
|
stampNativeScriptConfig(outDir, cfg);
|
|
1037
1059
|
stampIOSDisplayName(outDir, cfg, req);
|
|
1038
1060
|
stampTeamId(outDir, cfg, { cwd, configPath: resolveConfigPath(cwd, opts.flags ?? {}) });
|
|
1061
|
+
stampDeviceFamily(outDir, cfg);
|
|
1039
1062
|
stampAndroidAppName(outDir, cfg, req);
|
|
1040
1063
|
stampAndroidVersion(outDir, cfg);
|
|
1041
1064
|
stampAndroidGradleDeps(outDir, req.androidGradleDeps);
|
|
@@ -1158,6 +1181,14 @@ async function build(cwd: string, flags: Record<string, string>, positionals: st
|
|
|
1158
1181
|
console.error(`✖ Wrapper not found at ${outDir} — run \`appwrap init\` first`);
|
|
1159
1182
|
process.exit(1);
|
|
1160
1183
|
}
|
|
1184
|
+
// --build-number → process.env.APPWRAP_BUILD_NUMBER before sync() stamps the plist/gradle (same
|
|
1185
|
+
// mechanism `release` uses). Env-var path still honored by sync()'s buildNumberOf when no flag.
|
|
1186
|
+
try {
|
|
1187
|
+
applyBuildNumberFlag(flags['build-number']);
|
|
1188
|
+
} catch (e) {
|
|
1189
|
+
console.error(`✖ ${(e as Error).message}`);
|
|
1190
|
+
process.exit(1);
|
|
1191
|
+
}
|
|
1161
1192
|
// Make sure the wrapper reflects the latest config + PWA before compiling (also validates the config).
|
|
1162
1193
|
await sync(cwd, flags);
|
|
1163
1194
|
|
|
@@ -1207,11 +1238,18 @@ async function build(cwd: string, flags: Record<string, string>, positionals: st
|
|
|
1207
1238
|
* --build-number <n> set the store CFBundleVersion (sets APPWRAP_BUILD_NUMBER for the lane)
|
|
1208
1239
|
*
|
|
1209
1240
|
* Signing/ASC config is read from env by the lane (ASC_KEY_ID / ASC_ISSUER_ID / ASC_KEY_P8 /
|
|
1210
|
-
* MATCH_GIT_URL / MATCH_PASSWORD) — secrets never live in appwrap.config.
|
|
1211
|
-
|
|
1241
|
+
* MATCH_GIT_URL / MATCH_PASSWORD) — secrets never live in appwrap.config.
|
|
1242
|
+
*
|
|
1243
|
+
* `appwrap submit ios` reuses this same body with `lane: 'release'` → fastlane `:release`
|
|
1244
|
+
* (`upload_to_app_store`): a binary-only App Store production promote. Metadata/screenshots stay in
|
|
1245
|
+
* ASC; pass `--submit-for-review` to also submit the build for review. */
|
|
1246
|
+
async function release(cwd: string, flags: Record<string, string>, positionals: string[], lane: 'beta' | 'release' = 'beta'): Promise<void> {
|
|
1247
|
+
const submit = lane === 'release';
|
|
1248
|
+
const cmd = submit ? 'submit' : 'release';
|
|
1212
1249
|
const platform = positionals[0];
|
|
1213
1250
|
if (platform !== 'ios') {
|
|
1214
|
-
console.error(
|
|
1251
|
+
console.error(`Usage: appwrap ${cmd} ios [--server-url <url>] [--env <name>] [--build-number <n>]` +
|
|
1252
|
+
(submit ? ' [--submit-for-review]' : '') + ' [--config <path>] [--out native]\n' +
|
|
1215
1253
|
' (Android: `appwrap build android --release --aab` then `fastlane android beta`.)');
|
|
1216
1254
|
process.exit(1);
|
|
1217
1255
|
}
|
|
@@ -1239,36 +1277,42 @@ async function release(cwd: string, flags: Record<string, string>, positionals:
|
|
|
1239
1277
|
}
|
|
1240
1278
|
const stampCfg = serverUrl ? { ...cfg, loader: 'server' as const, serverUrl } : cfg;
|
|
1241
1279
|
|
|
1242
|
-
// Build number: explicit flag → APPWRAP_BUILD_NUMBER (
|
|
1243
|
-
//
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
}
|
|
1250
|
-
|
|
1280
|
+
// Build number: explicit flag → APPWRAP_BUILD_NUMBER. Stamping (sync → stampIOSDisplayName →
|
|
1281
|
+
// buildNumberOf) reads process.env.APPWRAP_BUILD_NUMBER, and sync() runs BEFORE fastlane archives
|
|
1282
|
+
// the (already-stamped) Info.plist — so the flag must land on process.env *here*, before sync(),
|
|
1283
|
+
// not only on the child env. In CI the workflow sets APPWRAP_BUILD_NUMBER itself (env path).
|
|
1284
|
+
try {
|
|
1285
|
+
applyBuildNumberFlag(flags['build-number']);
|
|
1286
|
+
} catch (e) {
|
|
1287
|
+
console.error(`✖ ${(e as Error).message}`);
|
|
1288
|
+
process.exit(1);
|
|
1251
1289
|
}
|
|
1290
|
+
const env = { ...process.env };
|
|
1291
|
+
// `submit ios --submit-for-review` → also submit the binary for App Store review (the lane defaults
|
|
1292
|
+
// to a safe binary-only promote). The flag is boolean (presence = '' via parseArgs).
|
|
1293
|
+
if (submit && 'submit-for-review' in flags) env.APPWRAP_SUBMIT_FOR_REVIEW = 'true';
|
|
1252
1294
|
|
|
1253
1295
|
// Re-stamp config + copy the latest PWA into native/ so the lane archives current sources. (The lane
|
|
1254
1296
|
// also runs `ns prepare ios --release`; sync here makes the wrapper config/PWA authoritative first.)
|
|
1255
1297
|
await sync(cwd, flags);
|
|
1256
1298
|
if (serverUrl) {
|
|
1257
1299
|
stampShellConfig(outDir, stampCfg);
|
|
1258
|
-
console.log(`✓ Release loader → ${serverUrl}${flags.env ? ` (env: ${flags.env})` : ''}`);
|
|
1300
|
+
console.log(`✓ ${submit ? 'Submit' : 'Release'} loader → ${serverUrl}${flags.env ? ` (env: ${flags.env})` : ''}`);
|
|
1259
1301
|
}
|
|
1260
1302
|
|
|
1261
|
-
console.log(`▶ fastlane ios
|
|
1303
|
+
console.log(`▶ fastlane ios ${lane} (cwd: ${outDir}/fastlane → native/)${env.APPWRAP_BUILD_NUMBER ? ` build #${env.APPWRAP_BUILD_NUMBER}` : ''}`);
|
|
1262
1304
|
try {
|
|
1263
|
-
execFileSync('fastlane', ['ios',
|
|
1305
|
+
execFileSync('fastlane', ['ios', lane], { cwd: outDir, stdio: 'inherit', env });
|
|
1264
1306
|
} catch {
|
|
1265
|
-
console.error(
|
|
1307
|
+
console.error(`\n✖ ${submit ? 'App Store submit' : 'TestFlight release'} failed. Common causes:\n` +
|
|
1266
1308
|
' • Missing ASC/match env: ASC_KEY_ID, ASC_ISSUER_ID, ASC_KEY_P8 (base64), MATCH_GIT_URL, MATCH_PASSWORD.\n' +
|
|
1267
1309
|
' • Certs/profiles not seeded — run `fastlane match appstore` once against MATCH_GIT_URL.\n' +
|
|
1268
1310
|
' • CFBundleVersion already used for this marketing version → pass a higher --build-number.');
|
|
1269
1311
|
process.exit(1);
|
|
1270
1312
|
}
|
|
1271
|
-
console.log(
|
|
1313
|
+
console.log(submit
|
|
1314
|
+
? `✓ Binary promoted to the App Store (production)${env.APPWRAP_SUBMIT_FOR_REVIEW === 'true' ? ' and submitted for review' : ' — submit for review in App Store Connect (or pass --submit-for-review)'}.`
|
|
1315
|
+
: '✓ Uploaded to TestFlight (App Store Connect processing — check the build list / wait for the email).');
|
|
1272
1316
|
}
|
|
1273
1317
|
|
|
1274
1318
|
interface AppleTeam { teamId: string; name: string; email?: string; paid: boolean }
|
|
@@ -1808,16 +1852,20 @@ async function main(): Promise<void> {
|
|
|
1808
1852
|
await deploy(cwd, flags, positionals);
|
|
1809
1853
|
break;
|
|
1810
1854
|
case 'release':
|
|
1811
|
-
await release(cwd, flags, positionals);
|
|
1855
|
+
await release(cwd, flags, positionals, 'beta');
|
|
1856
|
+
break;
|
|
1857
|
+
case 'submit':
|
|
1858
|
+
await release(cwd, flags, positionals, 'release');
|
|
1812
1859
|
break;
|
|
1813
1860
|
case 'logs':
|
|
1814
1861
|
await logs(cwd, flags, positionals);
|
|
1815
1862
|
break;
|
|
1816
1863
|
default:
|
|
1817
|
-
console.log('Usage: appwrap <init|sync|dev|build|deploy|release|logs> [--config <path>] [--out native]\n' +
|
|
1864
|
+
console.log('Usage: appwrap <init|sync|dev|build|deploy|release|submit|logs> [--config <path>] [--out native]\n' +
|
|
1818
1865
|
' config: appwrap.config.ts (preferred) → .js → appwrap.json\n' +
|
|
1819
1866
|
' build <ios|android> [--release] [--aab] deploy ios [--device <id|name>] [--no-launch]\n' +
|
|
1820
1867
|
' release ios [--server-url <url>] [--env <name>] [--build-number <n>] (build+sign+upload to TestFlight)\n' +
|
|
1868
|
+
' submit ios [--build-number <n>] [--submit-for-review] (promote the binary to the App Store; metadata stays in ASC)\n' +
|
|
1821
1869
|
' logs ios [--once] [--native] dev [--url <url> | --port <p>]');
|
|
1822
1870
|
process.exit(command ? 1 : 0);
|
|
1823
1871
|
}
|
package/src/config.ts
CHANGED
|
@@ -34,6 +34,11 @@ export interface AppwrapConfig {
|
|
|
34
34
|
* axis; `any` (default) leaves rotation free (sans upside-down on iOS). Stamped into iOS
|
|
35
35
|
* `UISupportedInterfaceOrientations` (+ `~ipad`) and Android `android:screenOrientation`. */
|
|
36
36
|
orientation?: 'portrait' | 'landscape' | 'any';
|
|
37
|
+
/** iOS targeted device family. `'iphone'` builds an iPhone-only app (TARGETED_DEVICE_FAMILY=1 →
|
|
38
|
+
* UIDeviceFamily=[1]); `'universal'` (default — NativeScript's behavior) targets iPhone + iPad,
|
|
39
|
+
* which makes the App Store require a full iPad screenshot set. Set `'iphone'` for a phone-only
|
|
40
|
+
* app to avoid that requirement. iOS-only; no-op on Android. */
|
|
41
|
+
targetedDevices?: 'iphone' | 'universal';
|
|
37
42
|
/** Android only (experimental). When true, the WebView draws genuinely edge-to-edge UNDER the
|
|
38
43
|
* transparent system bars (NS `androidOverflowEdge='dont-apply'`) and the real safe-area insets
|
|
39
44
|
* are injected as `--saie-*` CSS vars + native `env(safe-area-inset-*)`, so a multi-theme PWA
|
|
@@ -92,6 +97,15 @@ export interface AppwrapConfig {
|
|
|
92
97
|
* for in-WebView web-push as a fallback. NOTE: keeping the SW is the only way to get web push; native
|
|
93
98
|
* push is the separate `push` lane (APNs/FCM) and does NOT need a SW. */
|
|
94
99
|
neutralizeServiceWorker?: boolean;
|
|
100
|
+
/** iOS App-Bound Domains — the hosts (no scheme, max 10 per Apple) the WebView may navigate to.
|
|
101
|
+
* Setting this stamps `WKAppBoundDomains` into Info.plist AND sets
|
|
102
|
+
* `WKWebViewConfiguration.limitsNavigationsToAppBoundDomains = true`, which is Apple's HARD
|
|
103
|
+
* requirement for a service worker to run inside a WKWebView. Pair with `neutralizeServiceWorker:false`
|
|
104
|
+
* to actually enable a SW (this flag is the iOS gate; Android's SW is a separate lane).
|
|
105
|
+
* TRADEOFF: `limitsNavigationsToAppBoundDomains` RESTRICTS the WebView to exactly these domains —
|
|
106
|
+
* any navigation/OAuth/redirect to another domain breaks. Only for single-origin apps. Empty/unset =
|
|
107
|
+
* no change to current behavior (no Info.plist key, no nav restriction). */
|
|
108
|
+
appBoundDomains?: string[];
|
|
95
109
|
/** Hand navigations that LEAVE the app's own origin to the OS default browser (Safari / Chrome)
|
|
96
110
|
* instead of replacing the shell WebView — so external links and `window.open(...)`/`target="_blank"`
|
|
97
111
|
* behave like a regular native app (open in the system browser) rather than navigating away inside
|
package/src/derive.ts
CHANGED
|
@@ -97,6 +97,26 @@ export function resolveBuildNumber(
|
|
|
97
97
|
return deriveBuild(cfg.version);
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
/**
|
|
101
|
+
* Apply a `--build-number` CLI flag to the env so stamping picks it up. The stamping chain
|
|
102
|
+
* (sync → stampIOSDisplayName/gradle → buildNumberOf → resolveBuildNumber) reads
|
|
103
|
+
* `APPWRAP_BUILD_NUMBER` off the env, and stamping runs BEFORE the native build/archive — so the
|
|
104
|
+
* flag must land on the env *before* sync(), not only on a child process env copy.
|
|
105
|
+
* Returns the validated build-number string (set on `env`), or undefined if no flag was given.
|
|
106
|
+
* Throws on a non-positive-integer flag so the caller can print + exit.
|
|
107
|
+
*/
|
|
108
|
+
export function applyBuildNumberFlag(
|
|
109
|
+
flag: string | undefined,
|
|
110
|
+
env: { APPWRAP_BUILD_NUMBER?: string } = process.env
|
|
111
|
+
): string | undefined {
|
|
112
|
+
if (flag == null || flag === '') return undefined;
|
|
113
|
+
if (!/^\d+$/.test(flag)) {
|
|
114
|
+
throw new Error(`--build-number must be a positive integer (got "${flag}").`);
|
|
115
|
+
}
|
|
116
|
+
env.APPWRAP_BUILD_NUMBER = flag;
|
|
117
|
+
return flag;
|
|
118
|
+
}
|
|
119
|
+
|
|
100
120
|
/**
|
|
101
121
|
* Collapse a PWA web-manifest `orientation` value to our three states. The manifest spec allows
|
|
102
122
|
* `portrait`/`landscape` plus the `-primary`/`-secondary`/`*-up`/`natural` variants — we don't model
|
|
@@ -208,6 +228,27 @@ export function stampPlistBackgroundTasks(src: string, ids: string[] | undefined
|
|
|
208
228
|
return src;
|
|
209
229
|
}
|
|
210
230
|
|
|
231
|
+
/**
|
|
232
|
+
* Stamp (or strip) `WKAppBoundDomains` in an Info.plist string — Apple's gate for running a service
|
|
233
|
+
* worker inside a WKWebView (paired with `limitsNavigationsToAppBoundDomains` on the config). Pure +
|
|
234
|
+
* idempotent: rewrites an `appwrap:appbound` marker block (added before `</dict>`), so a re-sync never
|
|
235
|
+
* duplicates. `domains` empty/undefined → strips the block entirely (no key = current behavior). Apple
|
|
236
|
+
* caps the array at 10; we stamp what's given. Mirrors `stampPlistOrientations` (pure string transform).
|
|
237
|
+
*/
|
|
238
|
+
export function stampAppBoundDomains(src: string, domains: string[] | undefined): string {
|
|
239
|
+
src = src.replace(/\s*<!-- appwrap:appbound -->[\s\S]*?<!-- \/appwrap:appbound -->/g, '');
|
|
240
|
+
const list = (domains ?? []).filter(Boolean);
|
|
241
|
+
if (list.length) {
|
|
242
|
+
const items = list.map((d) => ` <string>${d}</string>`).join('\n');
|
|
243
|
+
const block =
|
|
244
|
+
` <!-- appwrap:appbound -->\n` +
|
|
245
|
+
` <key>WKAppBoundDomains</key>\n <array>\n${items}\n </array>\n` +
|
|
246
|
+
` <!-- /appwrap:appbound -->`;
|
|
247
|
+
src = src.replace(/<\/dict>\s*<\/plist>\s*$/, `${block}\n</dict>\n</plist>\n`);
|
|
248
|
+
}
|
|
249
|
+
return src;
|
|
250
|
+
}
|
|
251
|
+
|
|
211
252
|
/**
|
|
212
253
|
* Stamp the App Tracking Transparency declarations into PrivacyInfo.xcprivacy. Rewrites the two
|
|
213
254
|
* tracking keys IN PLACE (the template ships them, so we never restructure the doc — we only flip
|
|
@@ -36,44 +36,72 @@ def appwrap_prepare_ios
|
|
|
36
36
|
end
|
|
37
37
|
end
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
# Shared archive-and-sign sequence for BOTH the TestFlight (:beta) and App Store (:release) lanes.
|
|
40
|
+
# Returns the ASC API-key object so each lane can reuse it for its final upload step — the lanes
|
|
41
|
+
# differ ONLY in that final step (upload_to_testflight vs upload_to_app_store). Keep the recipe here,
|
|
42
|
+
# not duplicated per-lane.
|
|
43
|
+
def appwrap_archive_and_sign_ios
|
|
44
|
+
setup_ci # on CI: provisions a temporary unlocked keychain so codesign can use the match cert
|
|
43
45
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
46
|
+
api_key = app_store_connect_api_key(
|
|
47
|
+
key_id: ENV['ASC_KEY_ID'],
|
|
48
|
+
issuer_id: ENV['ASC_ISSUER_ID'],
|
|
49
|
+
key_content: ENV['ASC_KEY_P8'],
|
|
50
|
+
is_key_content_base64: true
|
|
51
|
+
)
|
|
50
52
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
# NativeScript prepares the Xcode project inside the wrapper dir (native/ = parent of fastlane/).
|
|
56
|
-
# Wrapped to sanitize the gem env so the system `pod` can find CocoaPods (see appwrap_prepare_ios).
|
|
57
|
-
appwrap_prepare_ios
|
|
58
|
-
|
|
59
|
-
# NS defaults the project to automatic signing → archive can't find a profile. Pin manual signing
|
|
60
|
-
# to the match-provided App Store profile (name exported by match into ENV).
|
|
61
|
-
profile = ENV["sigh_#{APP_ID}_appstore_profile-name"] || "match AppStore #{APP_ID}"
|
|
62
|
-
update_code_signing_settings(
|
|
63
|
-
use_automatic_signing: false,
|
|
64
|
-
path: 'platforms/ios/native.xcodeproj',
|
|
65
|
-
team_id: TEAM_ID,
|
|
66
|
-
bundle_identifier: APP_ID,
|
|
67
|
-
code_sign_identity: 'Apple Distribution',
|
|
68
|
-
profile_name: profile
|
|
69
|
-
)
|
|
53
|
+
# readonly: certs/profiles must be seeded once via `fastlane match appstore` (see workflow header).
|
|
54
|
+
match(type: 'appstore', readonly: true, app_identifier: APP_ID,
|
|
55
|
+
git_url: ENV['MATCH_GIT_URL'], api_key: api_key)
|
|
70
56
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
57
|
+
# NativeScript prepares the Xcode project inside the wrapper dir (native/ = parent of fastlane/).
|
|
58
|
+
# Wrapped to sanitize the gem env so the system `pod` can find CocoaPods (see appwrap_prepare_ios).
|
|
59
|
+
appwrap_prepare_ios
|
|
60
|
+
|
|
61
|
+
# NS defaults the project to automatic signing → archive can't find a profile. Pin manual signing
|
|
62
|
+
# to the match-provided App Store profile (name exported by match into ENV).
|
|
63
|
+
profile = ENV["sigh_#{APP_ID}_appstore_profile-name"] || "match AppStore #{APP_ID}"
|
|
64
|
+
update_code_signing_settings(
|
|
65
|
+
use_automatic_signing: false,
|
|
66
|
+
path: 'platforms/ios/native.xcodeproj',
|
|
67
|
+
team_id: TEAM_ID,
|
|
68
|
+
bundle_identifier: APP_ID,
|
|
69
|
+
code_sign_identity: 'Apple Distribution',
|
|
70
|
+
profile_name: profile
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
build_app(
|
|
74
|
+
workspace: 'platforms/ios/native.xcworkspace',
|
|
75
|
+
scheme: 'native',
|
|
76
|
+
export_method: 'app-store',
|
|
77
|
+
export_options: { provisioningProfiles: { APP_ID => profile } }
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
api_key
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Restore the login keychain as the system default and drop fastlane's temp keychain. `setup_ci`/`match`
|
|
84
|
+
# create `fastlane_tmp_keychain` and point the default keychain at it; if a lane dies mid-run before
|
|
85
|
+
# fastlane's own teardown, that stray keychain is left as the DEFAULT — so macOS (com.apple.iCloudHelper
|
|
86
|
+
# etc.) keeps prompting for its password forever. CI runners are ephemeral so this is a no-op there;
|
|
87
|
+
# locally it prevents the recurring keychain prompt. Best-effort: never fail the lane on cleanup.
|
|
88
|
+
def appwrap_keychain_cleanup
|
|
89
|
+
login = File.expand_path('~/Library/Keychains/login.keychain-db')
|
|
90
|
+
return unless File.exist?(login)
|
|
91
|
+
sh("security default-keychain -s '#{login}'", error_callback: ->(_) {})
|
|
92
|
+
sh('security delete-keychain ~/Library/Keychains/fastlane_tmp_keychain-db 2>/dev/null || true',
|
|
93
|
+
error_callback: ->(_) {})
|
|
94
|
+
rescue StandardError => e
|
|
95
|
+
UI.message("keychain cleanup skipped: #{e}")
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
platform :ios do
|
|
99
|
+
after_all { appwrap_keychain_cleanup }
|
|
100
|
+
error { |_lane, _exception| appwrap_keychain_cleanup }
|
|
101
|
+
|
|
102
|
+
desc 'Build the NativeScript wrapper and upload to TestFlight'
|
|
103
|
+
lane :beta do
|
|
104
|
+
api_key = appwrap_archive_and_sign_ios
|
|
77
105
|
|
|
78
106
|
# Wait for App Store Connect to finish processing and FAIL if it doesn't — Apple can accept the
|
|
79
107
|
# upload yet silently drop the build (e.g. an out-of-range CFBundleVersion). skip:true hides that.
|
|
@@ -85,6 +113,26 @@ platform :ios do
|
|
|
85
113
|
tf_wait_timeout = tf_wait_env.to_i > 0 ? tf_wait_env.to_i : 3600
|
|
86
114
|
upload_to_testflight(api_key: api_key, skip_waiting_for_build_processing: false, wait_processing_timeout_duration: tf_wait_timeout)
|
|
87
115
|
end
|
|
116
|
+
|
|
117
|
+
desc 'Build the NativeScript wrapper and promote the binary to the App Store (production).'
|
|
118
|
+
lane :release do
|
|
119
|
+
api_key = appwrap_archive_and_sign_ios
|
|
120
|
+
|
|
121
|
+
# Binary-only promote: the listing/metadata/screenshots/IAP stay in App Store Connect — this lane
|
|
122
|
+
# only ships the signed binary. SAFE by default: no review submit, no automatic release, headless
|
|
123
|
+
# (force: true → no HTML preview). Set APPWRAP_SUBMIT_FOR_REVIEW=true to also submit for review.
|
|
124
|
+
# Guard BOTH unset (nil) and blank env the same way as APPWRAP_TF_WAIT_TIMEOUT above.
|
|
125
|
+
submit_for_review = ENV['APPWRAP_SUBMIT_FOR_REVIEW'].to_s.strip.downcase == 'true'
|
|
126
|
+
upload_to_app_store(
|
|
127
|
+
api_key: api_key,
|
|
128
|
+
submit_for_review: submit_for_review,
|
|
129
|
+
automatic_release: false,
|
|
130
|
+
skip_metadata: true,
|
|
131
|
+
skip_screenshots: true,
|
|
132
|
+
precheck_include_in_app_purchases: false,
|
|
133
|
+
force: true
|
|
134
|
+
)
|
|
135
|
+
end
|
|
88
136
|
end
|
|
89
137
|
|
|
90
138
|
platform :android do
|