@livx.cc/appwrap 0.35.0 → 0.37.0
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/capabilities.manifest.ts +16 -1
- package/runtime/app/shell/devmenu.ts +49 -15
- package/runtime/app/shell/handlers-apple-signin.ts +169 -0
- package/runtime/app/shell/handlers.ts +1 -1
- package/runtime/app/shell/sha256.ts +78 -0
- package/src/cli.ts +108 -8
- package/templates/ci/fastlane/Fastfile +32 -1
- package/templates/ci/github/workflows/appwrap-release-ios.yml +10 -3
package/package.json
CHANGED
|
@@ -270,6 +270,21 @@ export const MODULES: ModuleManifest[] = [
|
|
|
270
270
|
ios: { permissions: [{ key: 'NSUserTrackingUsageDescription', domain: 'tracking', defaultUsage: 'Allow tracking to deliver a more personalized experience and measure ad performance.' }] },
|
|
271
271
|
},
|
|
272
272
|
|
|
273
|
+
// ── appleSignIn — native Sign in with Apple (iOS ASAuthorization) — opt-in, STRIPPABLE ──
|
|
274
|
+
// The native account-sheet alternative to the web-OAuth path: ASAuthorizationAppleIDProvider uses the
|
|
275
|
+
// App ID (bundle) and returns the identityToken + nonce DIRECTLY — no Services ID, no https Return URL,
|
|
276
|
+
// no browser redirect (which Apple rejects for custom-scheme redirect_uris). The PWA feeds the result
|
|
277
|
+
// to Firebase signInWithCredential('apple.com', { idToken, rawNonce }). iOS-only (`ios:true`/
|
|
278
|
+
// `android:false`) — Sign in with Apple has NO native Android SDK (the kit reports 'none' there and the
|
|
279
|
+
// app falls back to its web Apple auth). Stamps the `com.apple.developer.applesignin` entitlement ONLY
|
|
280
|
+
// when active (gated, like push's aps-environment) — a non-Apple-SignIn build signs without it. No
|
|
281
|
+
// permission string. Strippable own handler `handlers-apple-signin.ts` (in OPTIONAL_GROUPS).
|
|
282
|
+
{
|
|
283
|
+
name: 'appleSignIn', group: 'appleSignIn',
|
|
284
|
+
capabilities: { appleSignIn: { ios: true, android: false } },
|
|
285
|
+
ios: { entitlements: { 'com.apple.developer.applesignin': ['Default'] } },
|
|
286
|
+
},
|
|
287
|
+
|
|
273
288
|
// ── backgroundTask — headless background execution (HEADLESS JS HANDLER) — opt-in, STRIPPABLE ──
|
|
274
289
|
// The OS wakes the app (possibly cold, no visible WebView) for a permitted task id; the shell builds
|
|
275
290
|
// an OFFSCREEN WebView, loads the app conveying the id (the handshake reports it), awaits the JS
|
|
@@ -289,7 +304,7 @@ export const MODULES: ModuleManifest[] = [
|
|
|
289
304
|
|
|
290
305
|
/** Opt-in registration groups that own their own NS handler file (strippable when inactive). Core
|
|
291
306
|
* groups (core/extended/parity/system/media/billing) are always bundled; only these are CLI-gated. */
|
|
292
|
-
export const OPTIONAL_GROUPS = ['health', 'oauth', 'reviews', 'scanner', 'speech', 'tracking', 'backgroundTask'] as const;
|
|
307
|
+
export const OPTIONAL_GROUPS = ['health', 'oauth', 'reviews', 'scanner', 'speech', 'tracking', 'appleSignIn', 'backgroundTask'] as const;
|
|
293
308
|
|
|
294
309
|
/** Resolve the active capability map for the handshake from a set of active capability names. */
|
|
295
310
|
export function buildCapabilityMap(
|
|
@@ -17,30 +17,54 @@ export function startDevMenu(): void {
|
|
|
17
17
|
else if (isAndroid) startAndroidShake();
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
// ── shake detection
|
|
21
|
-
|
|
20
|
+
// ── shake detection — deliberate, robust against incidental motion (walking, bumps) ──
|
|
21
|
+
// A real "shake to open" is a sustained back-and-forth: several HARD spikes, close together, that
|
|
22
|
+
// REVERSE direction each time. Walking/jostle produces occasional same-direction spikes with gaps —
|
|
23
|
+
// so we require (a) a high threshold, (b) direction reversal between consecutive spikes, and (c) a
|
|
24
|
+
// short inter-spike gap that RESETS the count if you pause. The bar is a firm wrist-shake, not a step.
|
|
25
|
+
const SHAKE_G = 2.3; // hard spike vs. ~1g rest (was 1.8 — too low, walking cleared it)
|
|
26
|
+
const SHAKE_SPIKES = 4; // direction-reversing spikes needed (≈2 full back-and-forth shakes)
|
|
27
|
+
const SHAKE_GAP_MS = 400; // max gap between spikes; a longer pause resets the streak
|
|
28
|
+
const MENU_DEBOUNCE_MS = 1500; // don't re-open right after a menu
|
|
22
29
|
let shakeMm: CMMotionManager | null = null; // hold a ref so CoreMotion isn't GC'd
|
|
23
|
-
let firstSpikeAt = 0;
|
|
24
30
|
let spikes = 0;
|
|
31
|
+
let lastSpikeAt = 0;
|
|
32
|
+
let lastSpikeDir = 0; // sign of the dominant axis at the last spike; reversal = real shake
|
|
25
33
|
let lastMenuAt = 0;
|
|
26
34
|
|
|
27
|
-
|
|
35
|
+
/** @param g total accel magnitude in g (rest ≈ 1)
|
|
36
|
+
* @param dir sign (+1/-1) of the dominant-axis acceleration — used to detect back-and-forth reversal */
|
|
37
|
+
function onAccelMagnitude(g: number, dir: number): void {
|
|
28
38
|
if (g < SHAKE_G) return;
|
|
29
39
|
const now = Date.now();
|
|
30
|
-
if (now - lastMenuAt <
|
|
31
|
-
|
|
32
|
-
|
|
40
|
+
if (now - lastMenuAt < MENU_DEBOUNCE_MS) return;
|
|
41
|
+
// A spike that's too soon after the last counts as the SAME thrust, not a new one — ignore it so a
|
|
42
|
+
// single hard jolt's ringing doesn't rack up the count.
|
|
43
|
+
if (now - lastSpikeAt < 80) return;
|
|
44
|
+
// Reset the streak unless this spike continues a fast, direction-reversing sequence.
|
|
45
|
+
if (now - lastSpikeAt > SHAKE_GAP_MS || dir === lastSpikeDir) {
|
|
33
46
|
spikes = 1;
|
|
34
|
-
|
|
47
|
+
} else {
|
|
48
|
+
spikes++;
|
|
35
49
|
}
|
|
36
|
-
|
|
50
|
+
lastSpikeAt = now;
|
|
51
|
+
lastSpikeDir = dir;
|
|
52
|
+
if (spikes >= SHAKE_SPIKES) {
|
|
37
53
|
spikes = 0;
|
|
38
|
-
|
|
54
|
+
lastSpikeAt = 0;
|
|
55
|
+
lastSpikeDir = 0;
|
|
39
56
|
lastMenuAt = now;
|
|
40
57
|
void showDevMenu();
|
|
41
58
|
}
|
|
42
59
|
}
|
|
43
60
|
|
|
61
|
+
/** Sign of the largest-magnitude component — the axis the shake is thrusting along. */
|
|
62
|
+
function dominantDir(x: number, y: number, z: number): number {
|
|
63
|
+
const ax = Math.abs(x), ay = Math.abs(y), az = Math.abs(z);
|
|
64
|
+
const v = ax >= ay && ax >= az ? x : ay >= az ? y : z;
|
|
65
|
+
return v >= 0 ? 1 : -1;
|
|
66
|
+
}
|
|
67
|
+
|
|
44
68
|
function startIOSShake(): void {
|
|
45
69
|
// Poll `mm.deviceMotion` on a JS timer — CoreMotion's queue-handler block is fragile under
|
|
46
70
|
// NativeScript (can silently stop firing on-device); mirrors the motion.start handler.
|
|
@@ -52,10 +76,12 @@ function startIOSShake(): void {
|
|
|
52
76
|
setInterval(() => {
|
|
53
77
|
const m = mm.deviceMotion;
|
|
54
78
|
if (!m) return; // first sample not ready
|
|
55
|
-
const
|
|
56
|
-
const
|
|
57
|
-
const
|
|
58
|
-
|
|
79
|
+
const ux = m.userAcceleration.x, uy = m.userAcceleration.y, uz = m.userAcceleration.z;
|
|
80
|
+
const x = ux + m.gravity.x; // total accel in g (rest ≈ 1) — magnitude includes gravity
|
|
81
|
+
const y = uy + m.gravity.y;
|
|
82
|
+
const z = uz + m.gravity.z;
|
|
83
|
+
// Direction from gravity-free userAcceleration (gravity is constant → would mask the back-and-forth).
|
|
84
|
+
onAccelMagnitude(Math.sqrt(x * x + y * y + z * z), dominantDir(ux, uy, uz));
|
|
59
85
|
}, 100);
|
|
60
86
|
}
|
|
61
87
|
|
|
@@ -66,11 +92,19 @@ function startAndroidShake(): void {
|
|
|
66
92
|
const accel = sm.getDefaultSensor(android.hardware.Sensor.TYPE_ACCELEROMETER);
|
|
67
93
|
if (!accel) return;
|
|
68
94
|
const G = 9.80665;
|
|
95
|
+
// Low-pass gravity estimate so we can derive gravity-free direction (mirrors iOS userAcceleration);
|
|
96
|
+
// the raw accelerometer includes gravity, which is constant and would mask the back-and-forth.
|
|
97
|
+
const grav = [0, 0, 0];
|
|
98
|
+
const alpha = 0.8;
|
|
69
99
|
const listener = new android.hardware.SensorEventListener({
|
|
70
100
|
onAccuracyChanged() {},
|
|
71
101
|
onSensorChanged(e: android.hardware.SensorEvent) {
|
|
72
102
|
const v = e.values;
|
|
73
|
-
|
|
103
|
+
grav[0] = alpha * grav[0] + (1 - alpha) * v[0];
|
|
104
|
+
grav[1] = alpha * grav[1] + (1 - alpha) * v[1];
|
|
105
|
+
grav[2] = alpha * grav[2] + (1 - alpha) * v[2];
|
|
106
|
+
const lx = v[0] - grav[0], ly = v[1] - grav[1], lz = v[2] - grav[2]; // linear (gravity-free)
|
|
107
|
+
onAccelMagnitude(Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]) / G, dominantDir(lx, ly, lz));
|
|
74
108
|
},
|
|
75
109
|
});
|
|
76
110
|
sm.registerListener(listener, accel, android.hardware.SensorManager.SENSOR_DELAY_UI);
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { Utils, isIOS } from '@nativescript/core';
|
|
2
|
+
import { bridge } from './bridge';
|
|
3
|
+
import { sha256Hex } from './sha256';
|
|
4
|
+
|
|
5
|
+
const err = (code: string, message: string) => Object.assign(new Error(message), { code });
|
|
6
|
+
|
|
7
|
+
// Keep strong refs while the system sheet is up — ARC frees the controller + its delegate + context
|
|
8
|
+
// provider the moment the JS locals fall out of scope, which silently cancels the flow mid-handshake
|
|
9
|
+
// (same lesson as handlers-oauth.ts `activeSession`/`activeProvider`).
|
|
10
|
+
let activeController: ASAuthorizationController | null = null;
|
|
11
|
+
let activeDelegate: AppleSignInDelegate | null = null;
|
|
12
|
+
let activeProvider: AppleSignInContextProvider | null = null;
|
|
13
|
+
|
|
14
|
+
@NativeClass()
|
|
15
|
+
class AppleSignInContextProvider extends NSObject implements ASAuthorizationControllerPresentationContextProviding {
|
|
16
|
+
static ObjCProtocols = [ASAuthorizationControllerPresentationContextProviding];
|
|
17
|
+
static new(): AppleSignInContextProvider {
|
|
18
|
+
return <AppleSignInContextProvider>super.new();
|
|
19
|
+
}
|
|
20
|
+
presentationAnchorForAuthorizationController(_controller: ASAuthorizationController): UIWindow {
|
|
21
|
+
return (
|
|
22
|
+
Utils.ios.getRootViewController()?.view?.window ??
|
|
23
|
+
UIApplication.sharedApplication.keyWindow
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Decode an Apple-returned NSData JWT/code blob into a UTF-8 string (identityToken/authorizationCode). */
|
|
29
|
+
function dataToUtf8(data: NSData | null): string | undefined {
|
|
30
|
+
if (!data) return undefined;
|
|
31
|
+
const s = NSString.alloc().initWithDataEncoding(data, NSUTF8StringEncoding);
|
|
32
|
+
return s ? String(s) : undefined;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
@NativeClass()
|
|
36
|
+
class AppleSignInDelegate extends NSObject implements ASAuthorizationControllerDelegate {
|
|
37
|
+
static ObjCProtocols = [ASAuthorizationControllerDelegate];
|
|
38
|
+
// Per-call settlement (set right after `.new()`). The shared `settle` clears the ARC refs + recovers
|
|
39
|
+
// the WebView before resolving/rejecting, so it runs exactly once per flow.
|
|
40
|
+
rawNonce = '';
|
|
41
|
+
settle!: (fn: () => void) => void;
|
|
42
|
+
resolve!: (v: unknown) => void;
|
|
43
|
+
reject!: (e: unknown) => void;
|
|
44
|
+
static new(): AppleSignInDelegate {
|
|
45
|
+
return <AppleSignInDelegate>super.new();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
authorizationControllerDidCompleteWithAuthorization(
|
|
49
|
+
_controller: ASAuthorizationController,
|
|
50
|
+
authorization: ASAuthorization
|
|
51
|
+
): void {
|
|
52
|
+
const cred = authorization.credential as ASAuthorizationAppleIDCredential;
|
|
53
|
+
const identityToken = dataToUtf8(cred.identityToken ?? null);
|
|
54
|
+
if (!identityToken) {
|
|
55
|
+
this.settle(() => this.reject(err('NATIVE_ERROR', 'appleSignIn: Apple returned no identityToken')));
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const authorizationCode = dataToUtf8(cred.authorizationCode ?? null);
|
|
59
|
+
|
|
60
|
+
// First-authorization-only profile. Apple omits name/email on subsequent sign-ins per Apple ID.
|
|
61
|
+
let name: { givenName?: string; familyName?: string; displayName?: string } | undefined;
|
|
62
|
+
if (cred.fullName) {
|
|
63
|
+
const givenName = cred.fullName.givenName || undefined;
|
|
64
|
+
const familyName = cred.fullName.familyName || undefined;
|
|
65
|
+
// OS-composed display name (locale-aware) — convenience; the consumer can also build its own.
|
|
66
|
+
const composed = NSPersonNameComponentsFormatter.localizedStringFromPersonNameComponentsStyleOptions(
|
|
67
|
+
cred.fullName,
|
|
68
|
+
NSPersonNameComponentsFormatterStyle.Default,
|
|
69
|
+
0 as NSPersonNameComponentsFormatterOptions
|
|
70
|
+
);
|
|
71
|
+
const displayName = (composed && String(composed)) || undefined;
|
|
72
|
+
if (givenName || familyName || displayName) name = { givenName, familyName, displayName };
|
|
73
|
+
}
|
|
74
|
+
const email = cred.email || undefined;
|
|
75
|
+
const user = name || email ? { ...(name ? { name } : {}), ...(email ? { email } : {}) } : undefined;
|
|
76
|
+
|
|
77
|
+
this.settle(() =>
|
|
78
|
+
this.resolve({ identityToken, authorizationCode, nonce: this.rawNonce, ...(user ? { user } : {}) })
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
authorizationControllerDidCompleteWithError(_controller: ASAuthorizationController, error: NSError): void {
|
|
83
|
+
// ASAuthorizationError.Canceled === 1001 (user dismissed the sheet) → resolve cancelled, never throw.
|
|
84
|
+
if (error.code === ASAuthorizationError.Canceled) {
|
|
85
|
+
this.settle(() => this.resolve({ cancelled: true }));
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
this.settle(() => this.reject(err('NATIVE_ERROR', error.localizedDescription ?? 'appleSignIn failed')));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Native Sign in with Apple (iOS, `appleSignIn` module). Strippable own-handler file (registered only
|
|
94
|
+
* when the module is active) — a build without it compiles NO ASAuthorization code and stamps no
|
|
95
|
+
* entitlement. Presents `ASAuthorizationController` (the system account sheet) and returns the Apple
|
|
96
|
+
* `identityToken` + raw nonce DIRECTLY to JS, so the PWA can do `signInWithCredential` on Firebase.
|
|
97
|
+
*
|
|
98
|
+
* The caller passes a RAW nonce; we send SHA256(nonce) to Apple (Apple binds the id_token to the hash)
|
|
99
|
+
* and return the raw nonce unchanged so the web app hands `rawNonce` to Firebase.
|
|
100
|
+
*
|
|
101
|
+
* Name/email come back ONLY on the first authorization per Apple ID — we relay them when present.
|
|
102
|
+
* Cancel resolves `{ cancelled: true }` (never throws), matching the scanner/oauth contract.
|
|
103
|
+
*
|
|
104
|
+
* iOS-only: the capability is gated `ios:true`/`android:false`, so the kit short-circuits off iOS
|
|
105
|
+
* before reaching the bridge. The isIOS guard is defence-in-depth.
|
|
106
|
+
*
|
|
107
|
+
* DEVICE-VERIFIED (iPhone 13 Pro Max, 2026-06-24, hello-pwa under paid team RDYDSWE9RB): the native
|
|
108
|
+
* ASAuthorization sheet presented, returned a real Apple-signed identityToken (JWT) + the raw nonce +
|
|
109
|
+
* first-auth user email, and dismissed without freezing (recoverAfterNativeSurface held). The
|
|
110
|
+
* com.apple.developer.applesignin entitlement signs only on a paid team — the free personal team can't
|
|
111
|
+
* hold the Sign-in-with-Apple capability (same class as push aps-environment).
|
|
112
|
+
*/
|
|
113
|
+
export function registerAppleSignInHandlers(): void {
|
|
114
|
+
if (!isIOS) return;
|
|
115
|
+
|
|
116
|
+
bridge.register(
|
|
117
|
+
'appleSignIn.signIn',
|
|
118
|
+
({ nonce, scopes }: { nonce: string; scopes?: Array<'name' | 'email'> }) =>
|
|
119
|
+
new Promise((resolve, reject) => {
|
|
120
|
+
const rawNonce = String(nonce ?? '');
|
|
121
|
+
if (!rawNonce) { reject(err('NATIVE_ERROR', 'appleSignIn.signIn: empty nonce')); return; }
|
|
122
|
+
const want = new Set(scopes ?? ['name', 'email']);
|
|
123
|
+
|
|
124
|
+
Utils.dispatchToMainThread(() => {
|
|
125
|
+
try {
|
|
126
|
+
const provider = ASAuthorizationAppleIDProvider.alloc().init();
|
|
127
|
+
const request = provider.createRequest();
|
|
128
|
+
// Apple binds the returned id_token to SHA256(nonce); we hand back the RAW nonce to JS.
|
|
129
|
+
request.nonce = sha256Hex(rawNonce);
|
|
130
|
+
const scopeList: string[] = [];
|
|
131
|
+
if (want.has('name')) scopeList.push(ASAuthorizationScopeFullName);
|
|
132
|
+
if (want.has('email')) scopeList.push(ASAuthorizationScopeEmail);
|
|
133
|
+
request.requestedScopes = NSArray.arrayWithArray<string>(scopeList as any);
|
|
134
|
+
|
|
135
|
+
const settle = (fn: () => void) => {
|
|
136
|
+
activeController = null;
|
|
137
|
+
activeDelegate = null;
|
|
138
|
+
activeProvider = null;
|
|
139
|
+
// An ASAuthorization sheet can leave the WebView frozen on dismiss like the other native
|
|
140
|
+
// surfaces — recover from its completion (shared framework fix).
|
|
141
|
+
bridge.getWebView()?.recoverAfterNativeSurface();
|
|
142
|
+
fn();
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const delegate = AppleSignInDelegate.new();
|
|
146
|
+
delegate.rawNonce = rawNonce;
|
|
147
|
+
delegate.settle = settle;
|
|
148
|
+
delegate.resolve = resolve;
|
|
149
|
+
delegate.reject = reject;
|
|
150
|
+
|
|
151
|
+
const controller = ASAuthorizationController.alloc().initWithAuthorizationRequests(
|
|
152
|
+
NSArray.arrayWithObject<ASAuthorizationRequest>(request)
|
|
153
|
+
);
|
|
154
|
+
activeDelegate = delegate;
|
|
155
|
+
activeProvider = AppleSignInContextProvider.new();
|
|
156
|
+
controller.delegate = delegate;
|
|
157
|
+
controller.presentationContextProvider = activeProvider;
|
|
158
|
+
activeController = controller;
|
|
159
|
+
controller.performRequests();
|
|
160
|
+
} catch (e: any) {
|
|
161
|
+
activeController = null;
|
|
162
|
+
activeDelegate = null;
|
|
163
|
+
activeProvider = null;
|
|
164
|
+
reject(err('NATIVE_ERROR', e?.message ?? 'appleSignIn failed to start'));
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
})
|
|
168
|
+
);
|
|
169
|
+
}
|
|
@@ -10,7 +10,7 @@ import { ACTIVE_MODULE_NAMES } from './active-modules.generated';
|
|
|
10
10
|
import { consumePendingBackgroundTaskId } from './background-context';
|
|
11
11
|
|
|
12
12
|
/** Build identifier for the native shell bundle — bump per deploy to spot stale bundles. */
|
|
13
|
-
export const SHELL_BUILD = '
|
|
13
|
+
export const SHELL_BUILD = 'apple-signin-1';
|
|
14
14
|
|
|
15
15
|
/** Version status the web side (native-kit `kit.updates`) reports via `app.reportWebVersion`. */
|
|
16
16
|
export interface WebVersionInfo { current?: string; latest?: string; build?: string | number; updateAvailable?: boolean; }
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal, dependency-free SHA-256 → lowercase hex. Used by the `appleSignIn` handler to hash the raw
|
|
3
|
+
* nonce before sending it to Apple (Apple binds the returned id_token to SHA256(nonce); the raw nonce
|
|
4
|
+
* goes back to JS for Firebase). NativeScript ships no WebCrypto, and a pointer-juggling CommonCrypto
|
|
5
|
+
* FFI call is exactly the fragile any-typed interop the project avoids — a small pure-JS hash is
|
|
6
|
+
* deterministic and unit-testable without a device. Input is treated as a UTF-8 string.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const K = new Uint32Array([
|
|
10
|
+
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
|
|
11
|
+
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
|
|
12
|
+
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
|
13
|
+
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
|
|
14
|
+
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
|
|
15
|
+
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
|
16
|
+
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
|
|
17
|
+
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
function utf8Bytes(str: string): number[] {
|
|
21
|
+
const out: number[] = [];
|
|
22
|
+
for (let i = 0; i < str.length; i++) {
|
|
23
|
+
let c = str.charCodeAt(i);
|
|
24
|
+
if (c < 0x80) out.push(c);
|
|
25
|
+
else if (c < 0x800) out.push(0xc0 | (c >> 6), 0x80 | (c & 0x3f));
|
|
26
|
+
else if (c >= 0xd800 && c <= 0xdbff) {
|
|
27
|
+
// Surrogate pair → full code point.
|
|
28
|
+
const lo = str.charCodeAt(++i);
|
|
29
|
+
c = 0x10000 + ((c & 0x3ff) << 10) + (lo & 0x3ff);
|
|
30
|
+
out.push(0xf0 | (c >> 18), 0x80 | ((c >> 12) & 0x3f), 0x80 | ((c >> 6) & 0x3f), 0x80 | (c & 0x3f));
|
|
31
|
+
} else out.push(0xe0 | (c >> 12), 0x80 | ((c >> 6) & 0x3f), 0x80 | (c & 0x3f));
|
|
32
|
+
}
|
|
33
|
+
return out;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const rotr = (x: number, n: number) => (x >>> n) | (x << (32 - n));
|
|
37
|
+
|
|
38
|
+
/** SHA-256 of a UTF-8 string, lowercase hex (64 chars). */
|
|
39
|
+
export function sha256Hex(message: string): string {
|
|
40
|
+
const bytes = utf8Bytes(message);
|
|
41
|
+
const bitLen = bytes.length * 8;
|
|
42
|
+
bytes.push(0x80);
|
|
43
|
+
while (bytes.length % 64 !== 56) bytes.push(0);
|
|
44
|
+
// 64-bit big-endian length (high word is 0 for any realistic nonce).
|
|
45
|
+
for (let i = 7; i >= 0; i--) bytes.push((bitLen / 2 ** (8 * i)) & 0xff);
|
|
46
|
+
|
|
47
|
+
const h = new Uint32Array([
|
|
48
|
+
0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19,
|
|
49
|
+
]);
|
|
50
|
+
const w = new Uint32Array(64);
|
|
51
|
+
|
|
52
|
+
for (let i = 0; i < bytes.length; i += 64) {
|
|
53
|
+
for (let t = 0; t < 16; t++) {
|
|
54
|
+
w[t] = (bytes[i + 4 * t] << 24) | (bytes[i + 4 * t + 1] << 16) | (bytes[i + 4 * t + 2] << 8) | bytes[i + 4 * t + 3];
|
|
55
|
+
}
|
|
56
|
+
for (let t = 16; t < 64; t++) {
|
|
57
|
+
const s0 = rotr(w[t - 15], 7) ^ rotr(w[t - 15], 18) ^ (w[t - 15] >>> 3);
|
|
58
|
+
const s1 = rotr(w[t - 2], 17) ^ rotr(w[t - 2], 19) ^ (w[t - 2] >>> 10);
|
|
59
|
+
w[t] = (w[t - 16] + s0 + w[t - 7] + s1) | 0;
|
|
60
|
+
}
|
|
61
|
+
let [a, b, c, d, e, f, g, hh] = [h[0], h[1], h[2], h[3], h[4], h[5], h[6], h[7]];
|
|
62
|
+
for (let t = 0; t < 64; t++) {
|
|
63
|
+
const S1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25);
|
|
64
|
+
const ch = (e & f) ^ (~e & g);
|
|
65
|
+
const t1 = (hh + S1 + ch + K[t] + w[t]) | 0;
|
|
66
|
+
const S0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22);
|
|
67
|
+
const maj = (a & b) ^ (a & c) ^ (b & c);
|
|
68
|
+
const t2 = (S0 + maj) | 0;
|
|
69
|
+
hh = g; g = f; f = e; e = (d + t1) | 0; d = c; c = b; b = a; a = (t1 + t2) | 0;
|
|
70
|
+
}
|
|
71
|
+
h[0] = (h[0] + a) | 0; h[1] = (h[1] + b) | 0; h[2] = (h[2] + c) | 0; h[3] = (h[3] + d) | 0;
|
|
72
|
+
h[4] = (h[4] + e) | 0; h[5] = (h[5] + f) | 0; h[6] = (h[6] + g) | 0; h[7] = (h[7] + hh) | 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
let hex = '';
|
|
76
|
+
for (let i = 0; i < 8; i++) hex += (h[i] >>> 0).toString(16).padStart(8, '0');
|
|
77
|
+
return hex;
|
|
78
|
+
}
|
package/src/cli.ts
CHANGED
|
@@ -87,6 +87,12 @@ function resolveAssetRoot(rel: string): string {
|
|
|
87
87
|
const TEMPLATE_DIR = resolveAssetRoot('runtime');
|
|
88
88
|
const CI_TEMPLATE_DIR = resolveAssetRoot('templates/ci');
|
|
89
89
|
|
|
90
|
+
/** This CLI's own published version — used to pin the `bunx @livx.cc/appwrap@^x.y.z` invocations the
|
|
91
|
+
* emitted workflow runs. Pinning to THIS version's floor means CI fails LOUDLY ("version not found")
|
|
92
|
+
* until that version is published, instead of bunx silently resolving an older published build that
|
|
93
|
+
* lacks `release`/`init` flags. package.json sits one level above src/ (published) or the monorepo dir. */
|
|
94
|
+
const CLI_VERSION: string = (await import(resolve(import.meta.dir, '..', 'package.json'), { with: { type: 'json' } })).default.version;
|
|
95
|
+
|
|
90
96
|
// Load the capability manifest VALUES from the resolved runtime (pure data — safe outside NativeScript).
|
|
91
97
|
// Top-level await resolves before any command dispatches at the bottom of this file.
|
|
92
98
|
const { MODULES, OPTIONAL_GROUPS } = (await import(
|
|
@@ -179,6 +185,7 @@ const OPTIONAL_GROUP_HANDLERS: Record<string, { file: string; fn: string }> = {
|
|
|
179
185
|
scanner: { file: './handlers-scanner', fn: 'registerScannerHandlers' },
|
|
180
186
|
speech: { file: './handlers-speech', fn: 'registerSpeechHandlers' },
|
|
181
187
|
tracking: { file: './handlers-tracking', fn: 'registerTrackingHandlers' },
|
|
188
|
+
appleSignIn: { file: './handlers-apple-signin', fn: 'registerAppleSignInHandlers' },
|
|
182
189
|
backgroundTask: { file: './handlers-background', fn: 'registerBackgroundTaskHandlers' },
|
|
183
190
|
};
|
|
184
191
|
|
|
@@ -958,23 +965,34 @@ export function isFrameworkRepo(root: string): boolean {
|
|
|
958
965
|
return existsSync(join(root, 'packages/appwrap-cli/src/cli.ts'));
|
|
959
966
|
}
|
|
960
967
|
|
|
961
|
-
/** Emit CI scaffolding (GH Actions → git repo root, fastlane → native/).
|
|
962
|
-
|
|
968
|
+
/** Emit CI scaffolding (GH Actions → git repo root, fastlane → native/).
|
|
969
|
+
* GH workflows are never overwritten (users may customize them). The fastlane lane IS appwrap-managed
|
|
970
|
+
* (the release recipe, not for hand-editing — see AGENTS.md), so it is RE-EMITTED on `--force` to keep
|
|
971
|
+
* the recipe current after a framework upgrade; without --force it's still first-time-only. */
|
|
972
|
+
function copyCiTemplates(cwd: string, outDir: string, cfg: AppwrapConfig, force = false): void {
|
|
963
973
|
if (!existsSync(CI_TEMPLATE_DIR)) return;
|
|
964
974
|
const repoRoot = gitRoot(cwd);
|
|
965
975
|
// GitHub only reads `.github/workflows` at the REPO ROOT — in a monorepo, writing it under the
|
|
966
976
|
// package cwd (e.g. packages/app/.github) is dead config and regenerates a stray workflow each init.
|
|
967
|
-
|
|
977
|
+
// [from, to, overwritable]
|
|
978
|
+
const targets: Array<[string, string, boolean]> = [[join(CI_TEMPLATE_DIR, 'fastlane'), join(outDir, 'fastlane'), force]];
|
|
968
979
|
// …but if the repo root IS the appwrap framework itself (in-repo example), DON'T scaffold consumer
|
|
969
980
|
// workflows into the framework's .github — that's the stray-workflow-each-init bug.
|
|
970
981
|
if (isFrameworkRepo(repoRoot)) {
|
|
971
982
|
console.log(' ci ← GH Actions scaffold skipped (inside the appwrap framework repo — manages its own CI)');
|
|
972
983
|
} else {
|
|
973
|
-
targets.unshift([join(CI_TEMPLATE_DIR, 'github/workflows'), join(repoRoot, '.github/workflows')]);
|
|
984
|
+
targets.unshift([join(CI_TEMPLATE_DIR, 'github/workflows'), join(repoRoot, '.github/workflows'), false]);
|
|
974
985
|
}
|
|
975
|
-
for (const [from, to] of targets) {
|
|
986
|
+
for (const [from, to, overwrite] of targets) {
|
|
976
987
|
mkdirSync(to, { recursive: true });
|
|
977
|
-
cpSync(from, to, { recursive: true, force:
|
|
988
|
+
cpSync(from, to, { recursive: true, force: overwrite, errorOnExist: false });
|
|
989
|
+
}
|
|
990
|
+
// Pin the emitted workflow's `bunx @livx.cc/appwrap@^x.y.z` to THIS CLI's version floor, so a CI run
|
|
991
|
+
// using a freshly-emitted workflow can't silently resolve an older published build that lacks the
|
|
992
|
+
// `init`/`release` commands (it would 404 loudly instead). Idempotent: re-init finds no placeholder.
|
|
993
|
+
if (!isFrameworkRepo(repoRoot)) {
|
|
994
|
+
const wf = join(repoRoot, '.github/workflows/appwrap-release-ios.yml');
|
|
995
|
+
if (existsSync(wf)) writeFileSync(wf, readFileSync(wf, 'utf8').replaceAll('__APPWRAP_VERSION__', CLI_VERSION));
|
|
978
996
|
}
|
|
979
997
|
// Stamp the app id + team into the emitted fastlane (signing needs them; the templates ship
|
|
980
998
|
// `__APP_ID__`/`__TEAM_ID__` placeholders). Idempotent: re-init finds no placeholders → no-op.
|
|
@@ -1059,7 +1077,7 @@ async function init(cwd: string, flags: Record<string, string>): Promise<void> {
|
|
|
1059
1077
|
console.log(`🎁 appwrap init → ${outDir}`);
|
|
1060
1078
|
mkdirSync(outDir, { recursive: true });
|
|
1061
1079
|
regenerateCore(cwd, outDir, cfg, { firstRun: true, flags });
|
|
1062
|
-
copyCiTemplates(cwd, outDir, cfg); // first-time
|
|
1080
|
+
copyCiTemplates(cwd, outDir, cfg, 'force' in flags); // GH workflows: first-time only; fastlane lane: re-emit on --force
|
|
1063
1081
|
writeFileSync(join(outDir, '.gitignore'), 'node_modules/\nplatforms/\nhooks/\n');
|
|
1064
1082
|
applyOverrides(cwd, outDir, cfg); // escape hatch — last, so custom native code wins
|
|
1065
1083
|
stampVersionManifest(outDir, cfg); // provenance — also marks the dir appwrap-managed
|
|
@@ -1175,6 +1193,84 @@ async function build(cwd: string, flags: Record<string, string>, positionals: st
|
|
|
1175
1193
|
}
|
|
1176
1194
|
}
|
|
1177
1195
|
|
|
1196
|
+
/** `appwrap release ios` — the ONE build+sign+upload-to-TestFlight command, identical locally and in CI.
|
|
1197
|
+
*
|
|
1198
|
+
* It re-stamps the config + PWA (`sync`) and then delegates the full archive/sign/upload to the emitted
|
|
1199
|
+
* fastlane lane (`native/fastlane` `:beta`) — the SINGLE source of truth for the iOS release recipe (the
|
|
1200
|
+
* lane runs `ns prepare ios --release` → match signing → build_app → upload_to_testflight). CI is a thin
|
|
1201
|
+
* wrapper that just calls this. Keeping the recipe in fastlane (not duplicated in TS) means local and CI
|
|
1202
|
+
* run byte-identical steps.
|
|
1203
|
+
*
|
|
1204
|
+
* Knobs (all optional; mirror the workflow):
|
|
1205
|
+
* --server-url <url> override loader:'server' serverUrl for this release (lab vs prod backend)
|
|
1206
|
+
* --env <name> convenience: resolve serverUrl from cfg.envs[name] when present (see config)
|
|
1207
|
+
* --build-number <n> set the store CFBundleVersion (sets APPWRAP_BUILD_NUMBER for the lane)
|
|
1208
|
+
*
|
|
1209
|
+
* 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
|
+
async function release(cwd: string, flags: Record<string, string>, positionals: string[]): Promise<void> {
|
|
1212
|
+
const platform = positionals[0];
|
|
1213
|
+
if (platform !== 'ios') {
|
|
1214
|
+
console.error('Usage: appwrap release ios [--server-url <url>] [--env <name>] [--build-number <n>] [--config <path>] [--out native]\n' +
|
|
1215
|
+
' (Android: `appwrap build android --release --aab` then `fastlane android beta`.)');
|
|
1216
|
+
process.exit(1);
|
|
1217
|
+
}
|
|
1218
|
+
const cfg = await loadConfig(cwd, flags);
|
|
1219
|
+
const outDir = resolve(cwd, flags.out ?? 'native');
|
|
1220
|
+
if (!existsSync(outDir)) {
|
|
1221
|
+
console.error(`✖ Wrapper not found at ${outDir} — run \`appwrap init\` first (CI must \`init\`, native/ is gitignored).`);
|
|
1222
|
+
process.exit(1);
|
|
1223
|
+
}
|
|
1224
|
+
const fastfile = join(outDir, 'fastlane', 'Fastfile');
|
|
1225
|
+
if (!existsSync(fastfile)) {
|
|
1226
|
+
console.error(`✖ No fastlane lane at ${fastfile} — run \`appwrap init\` to emit it (it carries the release recipe).`);
|
|
1227
|
+
process.exit(1);
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
// Optional backend-url override for loader:'server' apps (lab vs prod). --server-url wins; else
|
|
1231
|
+
// --env resolves from cfg.envs[name] when the consumer config declares it.
|
|
1232
|
+
let serverUrl = flags['server-url'] || undefined;
|
|
1233
|
+
if (!serverUrl && flags.env) {
|
|
1234
|
+
serverUrl = (cfg as { envs?: Record<string, string> }).envs?.[flags.env];
|
|
1235
|
+
if (!serverUrl) {
|
|
1236
|
+
console.error(`✖ --env ${flags.env} given but cfg.envs[${flags.env}] is not set in the config.`);
|
|
1237
|
+
process.exit(1);
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
const stampCfg = serverUrl ? { ...cfg, loader: 'server' as const, serverUrl } : cfg;
|
|
1241
|
+
|
|
1242
|
+
// Build number: explicit flag → APPWRAP_BUILD_NUMBER (which the CLI's stamping already honors and
|
|
1243
|
+
// wins over the derived default). In CI the workflow sets APPWRAP_BUILD_NUMBER itself.
|
|
1244
|
+
const env = { ...process.env };
|
|
1245
|
+
if (flags['build-number']) {
|
|
1246
|
+
if (!/^\d+$/.test(flags['build-number'])) {
|
|
1247
|
+
console.error(`✖ --build-number must be a positive integer (got "${flags['build-number']}").`);
|
|
1248
|
+
process.exit(1);
|
|
1249
|
+
}
|
|
1250
|
+
env.APPWRAP_BUILD_NUMBER = flags['build-number'];
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
// Re-stamp config + copy the latest PWA into native/ so the lane archives current sources. (The lane
|
|
1254
|
+
// also runs `ns prepare ios --release`; sync here makes the wrapper config/PWA authoritative first.)
|
|
1255
|
+
await sync(cwd, flags);
|
|
1256
|
+
if (serverUrl) {
|
|
1257
|
+
stampShellConfig(outDir, stampCfg);
|
|
1258
|
+
console.log(`✓ Release loader → ${serverUrl}${flags.env ? ` (env: ${flags.env})` : ''}`);
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
console.log(`▶ fastlane ios beta (cwd: ${outDir}/fastlane → native/)${env.APPWRAP_BUILD_NUMBER ? ` build #${env.APPWRAP_BUILD_NUMBER}` : ''}`);
|
|
1262
|
+
try {
|
|
1263
|
+
execFileSync('fastlane', ['ios', 'beta'], { cwd: outDir, stdio: 'inherit', env });
|
|
1264
|
+
} catch {
|
|
1265
|
+
console.error('\n✖ TestFlight release failed. Common causes:\n' +
|
|
1266
|
+
' • Missing ASC/match env: ASC_KEY_ID, ASC_ISSUER_ID, ASC_KEY_P8 (base64), MATCH_GIT_URL, MATCH_PASSWORD.\n' +
|
|
1267
|
+
' • Certs/profiles not seeded — run `fastlane match appstore` once against MATCH_GIT_URL.\n' +
|
|
1268
|
+
' • CFBundleVersion already used for this marketing version → pass a higher --build-number.');
|
|
1269
|
+
process.exit(1);
|
|
1270
|
+
}
|
|
1271
|
+
console.log('✓ Uploaded to TestFlight (App Store Connect processing — check the build list / wait for the email).');
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1178
1274
|
interface AppleTeam { teamId: string; name: string; email?: string; paid: boolean }
|
|
1179
1275
|
interface DeviceInfo { id: string; name: string; model: string; transport: string }
|
|
1180
1276
|
|
|
@@ -1711,13 +1807,17 @@ async function main(): Promise<void> {
|
|
|
1711
1807
|
case 'deploy':
|
|
1712
1808
|
await deploy(cwd, flags, positionals);
|
|
1713
1809
|
break;
|
|
1810
|
+
case 'release':
|
|
1811
|
+
await release(cwd, flags, positionals);
|
|
1812
|
+
break;
|
|
1714
1813
|
case 'logs':
|
|
1715
1814
|
await logs(cwd, flags, positionals);
|
|
1716
1815
|
break;
|
|
1717
1816
|
default:
|
|
1718
|
-
console.log('Usage: appwrap <init|sync|dev|build|deploy|logs> [--config <path>] [--out native]\n' +
|
|
1817
|
+
console.log('Usage: appwrap <init|sync|dev|build|deploy|release|logs> [--config <path>] [--out native]\n' +
|
|
1719
1818
|
' config: appwrap.config.ts (preferred) → .js → appwrap.json\n' +
|
|
1720
1819
|
' build <ios|android> [--release] [--aab] deploy ios [--device <id|name>] [--no-launch]\n' +
|
|
1820
|
+
' release ios [--server-url <url>] [--env <name>] [--build-number <n>] (build+sign+upload to TestFlight)\n' +
|
|
1721
1821
|
' logs ios [--once] [--native] dev [--url <url> | --port <p>]');
|
|
1722
1822
|
process.exit(command ? 1 : 0);
|
|
1723
1823
|
}
|
|
@@ -6,6 +6,36 @@ default_platform(:ios)
|
|
|
6
6
|
APP_ID = '__APP_ID__'.freeze
|
|
7
7
|
TEAM_ID = '__TEAM_ID__'.freeze
|
|
8
8
|
|
|
9
|
+
# Run `ns prepare` (which shells out to `pod install`) with a SANITIZED gem env.
|
|
10
|
+
#
|
|
11
|
+
# Why: fastlane runs under its own Ruby and exports GEM_HOME/GEM_PATH into every child process. On a
|
|
12
|
+
# Mac, NativeScript's `pod` is the system one (`/usr/local/bin/pod`, shebang-pinned to Ruby 2.6, whose
|
|
13
|
+
# CocoaPods lives in /Library/Ruby/Gems/2.6.0). The inherited GEM_HOME redirects its gem lookup to
|
|
14
|
+
# fastlane's Ruby dir → `can't find gem cocoapods (Gem::GemNotFoundException)` → NS prints
|
|
15
|
+
# `'arch install' command failed` (exit 127). Interactively `pod` works because no GEM_HOME is set.
|
|
16
|
+
#
|
|
17
|
+
# Fix: only when the active `pod` is that system-Ruby-2.6 binary, drop the leaked GEM_* vars so it
|
|
18
|
+
# falls back to its own default gem dir. CI runners use a self-managed Ruby (setup-ruby) whose `pod`
|
|
19
|
+
# is NOT the system 2.6 one, so the env is left untouched there — no behavior change on CI.
|
|
20
|
+
def appwrap_prepare_ios
|
|
21
|
+
prepare = "cd '#{File.expand_path('..', __dir__)}' && ns prepare ios --release"
|
|
22
|
+
pod = `command -v pod`.strip
|
|
23
|
+
system_pod = !pod.empty? && File.exist?(pod) &&
|
|
24
|
+
File.read(pod, 256).include?('Ruby.framework/Versions/2.6')
|
|
25
|
+
unless system_pod
|
|
26
|
+
sh(prepare)
|
|
27
|
+
return
|
|
28
|
+
end
|
|
29
|
+
leaked = %w[GEM_HOME GEM_PATH RUBYLIB RUBYOPT BUNDLE_GEMFILE BUNDLE_BIN_PATH]
|
|
30
|
+
saved = {}
|
|
31
|
+
leaked.each { |k| saved[k] = ENV[k]; ENV.delete(k) }
|
|
32
|
+
begin
|
|
33
|
+
sh(prepare)
|
|
34
|
+
ensure
|
|
35
|
+
saved.each { |k, v| ENV[k] = v unless v.nil? }
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
9
39
|
platform :ios do
|
|
10
40
|
desc 'Build the NativeScript wrapper and upload to TestFlight'
|
|
11
41
|
lane :beta do
|
|
@@ -23,7 +53,8 @@ platform :ios do
|
|
|
23
53
|
git_url: ENV['MATCH_GIT_URL'], api_key: api_key)
|
|
24
54
|
|
|
25
55
|
# NativeScript prepares the Xcode project inside the wrapper dir (native/ = parent of fastlane/).
|
|
26
|
-
|
|
56
|
+
# Wrapped to sanitize the gem env so the system `pod` can find CocoaPods (see appwrap_prepare_ios).
|
|
57
|
+
appwrap_prepare_ios
|
|
27
58
|
|
|
28
59
|
# NS defaults the project to automatic signing → archive can't find a profile. Pin manual signing
|
|
29
60
|
# to the match-provided App Store profile (name exported by match into ENV).
|
|
@@ -34,7 +34,13 @@ jobs:
|
|
|
34
34
|
- run: bun install && bun run build
|
|
35
35
|
# Pin global tools — unpinned installs can grab a breaking release mid-flight. Bump deliberately.
|
|
36
36
|
- run: npm i -g nativescript@9.0.6
|
|
37
|
-
|
|
37
|
+
# bunx is PINNED to the appwrap version that emitted this workflow (stamped at `init`). Pinning is
|
|
38
|
+
# load-bearing: an UNPINNED `bunx @livx.cc/appwrap` resolves to whatever is currently published,
|
|
39
|
+
# which may pre-date the `init`/`release` commands and would silently run old code + exit 1 with a
|
|
40
|
+
# confusing usage dump. With the pin, CI fails LOUDLY ("No matching version") until that version is
|
|
41
|
+
# published to the @livx.cc registry. PUBLISH-ORDERING: publish the appwrap CLI BEFORE the first CI
|
|
42
|
+
# run that uses a freshly-emitted workflow.
|
|
43
|
+
- run: bunx @livx.cc/appwrap@^__APPWRAP_VERSION__ init # native/ is generated (gitignored) — regenerate it fresh in CI
|
|
38
44
|
- working-directory: native
|
|
39
45
|
run: npm install
|
|
40
46
|
- uses: ruby/setup-ruby@v1
|
|
@@ -47,14 +53,15 @@ jobs:
|
|
|
47
53
|
chmod 600 ~/.ssh/match_deploy_key
|
|
48
54
|
printf 'Host github.com\n IdentityFile ~/.ssh/match_deploy_key\n IdentitiesOnly yes\n' >> ~/.ssh/config
|
|
49
55
|
ssh-keyscan github.com >> ~/.ssh/known_hosts 2>/dev/null
|
|
56
|
+
# Single source of truth: the SAME command a developer runs locally. It re-stamps native/ and
|
|
57
|
+
# delegates the full archive/sign/upload to the emitted fastlane lane — no fastlane logic here.
|
|
50
58
|
- name: Build & upload to TestFlight
|
|
51
|
-
working-directory: native
|
|
52
59
|
env:
|
|
53
60
|
MATCH_GIT_URL: ${{ secrets.MATCH_GIT_URL }}
|
|
54
61
|
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
|
|
55
62
|
ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
|
|
56
63
|
ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
|
|
57
64
|
ASC_KEY_P8: ${{ secrets.ASC_KEY_P8 }}
|
|
58
|
-
run:
|
|
65
|
+
run: bunx @livx.cc/appwrap@^__APPWRAP_VERSION__ release ios
|
|
59
66
|
|
|
60
67
|
# Production promotion is a manual gate: promote the TestFlight build in App Store Connect.
|