@livx.cc/appwrap 0.28.2 → 0.30.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 +4 -0
- package/runtime/app/shell/handlers-billing.ts +19 -1
- package/runtime/modules-native/billing/App_Resources/iOS/src/AppwrapManageSubscriptions.swift +39 -0
- package/src/cli.ts +7 -22
- package/src/config.ts +6 -2
- package/src/derive.ts +50 -0
- package/templates/ci/fastlane/Fastfile +7 -1
package/package.json
CHANGED
|
@@ -141,6 +141,10 @@ export const MODULES: ModuleManifest[] = [
|
|
|
141
141
|
{
|
|
142
142
|
name: 'billing', group: 'billing',
|
|
143
143
|
capabilities: { billing: { ios: true, android: false } },
|
|
144
|
+
// iOS-only Swift shim (AppwrapManageSubscriptions.swift) bridging StoreKit 2's Swift-async
|
|
145
|
+
// showManageSubscriptions(in:) to an @objc completion the ObjC bridge can call. Compiled into
|
|
146
|
+
// the app target only when `billing` is active (NS auto-compiles App_Resources/iOS/src/*.swift).
|
|
147
|
+
nativeSrc: 'billing',
|
|
144
148
|
},
|
|
145
149
|
|
|
146
150
|
// ── reviews — in-app store review prompt — opt-in, STRIPPABLE (own handler + group) ──
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { Utils, isIOS } from '@nativescript/core';
|
|
2
2
|
import { bridge } from './bridge';
|
|
3
3
|
|
|
4
|
+
// @objc Swift shim (AppwrapManageSubscriptions.swift, billing nativeSrc) — no NS types; bridges
|
|
5
|
+
// StoreKit 2's Swift-async showManageSubscriptions(in:) to a completion the ObjC bridge can call.
|
|
6
|
+
declare const AppwrapManageSubscriptions: { present(completion: (message: string | null) => void): void };
|
|
7
|
+
|
|
4
8
|
/**
|
|
5
9
|
* In-app purchases — iOS StoreKit 1 (SKPaymentQueue). StoreKit 2 (Product/Transaction)
|
|
6
10
|
* is Swift-only and unreachable via the ObjC bridge, so we use the delegate-based
|
|
@@ -199,9 +203,23 @@ export function registerBillingHandlers(): void {
|
|
|
199
203
|
});
|
|
200
204
|
|
|
201
205
|
bridge.register('billing.manageSubscriptions', () => {
|
|
202
|
-
// StoreKit
|
|
206
|
+
// DEFAULT: StoreKit 1 deep-link — correct for production, but leaves the app and doesn't show
|
|
207
|
+
// sandbox/TestFlight subs. The in-app sheet is the opt-in `billing.manageSubscriptionsSheet` below.
|
|
203
208
|
Utils.openUrl('itms-apps://apps.apple.com/account/subscriptions');
|
|
204
209
|
});
|
|
210
|
+
|
|
211
|
+
// OPT-IN (iOS 15+): StoreKit 2's in-app management sheet via the AppwrapManageSubscriptions Swift
|
|
212
|
+
// shim — shows the user's subs (incl. sandbox/TestFlight) and lets them cancel without leaving the
|
|
213
|
+
// app. Reached only when the app calls kit.billing.showManageSubscriptionsSheet(), so the default
|
|
214
|
+
// deep-link path above is untouched. Rejects on iOS <15, no scene, or a StoreKit error.
|
|
215
|
+
bridge.register('billing.manageSubscriptionsSheet', () => {
|
|
216
|
+
return new Promise<void>((resolve, reject) => {
|
|
217
|
+
AppwrapManageSubscriptions.present((message: string | null) => {
|
|
218
|
+
if (message) reject(err('NATIVE_ERROR', message));
|
|
219
|
+
else resolve();
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
});
|
|
205
223
|
}
|
|
206
224
|
|
|
207
225
|
function restoreTransactions(): Promise<any[]> {
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import StoreKit
|
|
3
|
+
import UIKit
|
|
4
|
+
|
|
5
|
+
// StoreKit 2's `AppStore.showManageSubscriptions(in:)` is Swift-only + async, so it can't be reached
|
|
6
|
+
// from the ObjC bridge NativeScript uses. This @objc wrapper bridges the async call to a completion
|
|
7
|
+
// callback (NativeScript can't await Swift async) and runs it on the main actor against the active
|
|
8
|
+
// foreground UIWindowScene. Shipped via the billing module's nativeSrc → compiled into the app target
|
|
9
|
+
// only when `billing` is active. iOS 15+ (StoreKit 2); the JS layer guards the deep-link fallback.
|
|
10
|
+
@objc(AppwrapManageSubscriptions)
|
|
11
|
+
public final class AppwrapManageSubscriptions: NSObject {
|
|
12
|
+
// completion(nil) on success; completion(message) on failure. Always invoked on the main thread
|
|
13
|
+
// so the NativeScript JS continuation (and WebView response delivery) resumes on the UI thread.
|
|
14
|
+
@objc public static func present(_ completion: @escaping (String?) -> Void) {
|
|
15
|
+
guard #available(iOS 15.0, *) else {
|
|
16
|
+
completion("StoreKit 2 manage-subscriptions sheet requires iOS 15+")
|
|
17
|
+
return
|
|
18
|
+
}
|
|
19
|
+
Task { @MainActor in
|
|
20
|
+
guard let scene = AppwrapManageSubscriptions.activeWindowScene() else {
|
|
21
|
+
completion("No active UIWindowScene to present the subscriptions sheet")
|
|
22
|
+
return
|
|
23
|
+
}
|
|
24
|
+
do {
|
|
25
|
+
try await AppStore.showManageSubscriptions(in: scene)
|
|
26
|
+
completion(nil)
|
|
27
|
+
} catch {
|
|
28
|
+
completion(error.localizedDescription)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// The foreground-active window scene (the one allowed to present UI). Falls back to any connected
|
|
34
|
+
// window scene so a not-yet-foregroundActive launch still finds a presenter.
|
|
35
|
+
@MainActor private static func activeWindowScene() -> UIWindowScene? {
|
|
36
|
+
let scenes = UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }
|
|
37
|
+
return scenes.first { $0.activationState == .foregroundActive } ?? scenes.first
|
|
38
|
+
}
|
|
39
|
+
}
|
package/src/cli.ts
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
androidScreenOrientation,
|
|
25
25
|
iosOrientations,
|
|
26
26
|
mergeManifest,
|
|
27
|
+
resolveBuildNumber,
|
|
27
28
|
stampAndroidOrientation,
|
|
28
29
|
stampAndroidQueries,
|
|
29
30
|
stampPlistBackgroundTasks,
|
|
@@ -45,31 +46,15 @@ const execErrText = (e: unknown): string => {
|
|
|
45
46
|
return `${err.stdout ?? ''}${err.stderr ?? ''}`;
|
|
46
47
|
};
|
|
47
48
|
|
|
48
|
-
/** Marketing version → a monotonic integer build (0.2.1 → 201; 1.4.12 → 10412). Stable & increasing
|
|
49
|
-
* across semver bumps so store re-uploads are always accepted without a manual bump. */
|
|
50
|
-
function deriveBuild(version: string): number {
|
|
51
|
-
const [maj = 0, min = 0, patch = 0] = version.split('.').map((n) => parseInt(n, 10) || 0);
|
|
52
|
-
return maj * 10000 + min * 100 + patch;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
49
|
/**
|
|
56
|
-
* Resolved monotonic build number (iOS CFBundleVersion / Android versionCode).
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
* of
|
|
50
|
+
* Resolved monotonic build number (iOS CFBundleVersion / Android versionCode). Thin env wrapper over
|
|
51
|
+
* the pure `resolveBuildNumber` (in derive.ts, where it unit-tests). Precedence: `APPWRAP_BUILD_NUMBER`
|
|
52
|
+
* env (CI run #) > explicit numeric `cfg.buildNumber` > named strategy ('timestamp'|'epoch') > derived
|
|
53
|
+
* from version. The env override gives CI a monotonic, collision-free build for free; the derived
|
|
54
|
+
* default is CONSTANT per version, so repeat uploads of one marketing version would 409 without it.
|
|
61
55
|
*/
|
|
62
56
|
function buildNumberOf(cfg: AppwrapConfig): number {
|
|
63
|
-
|
|
64
|
-
if (env != null && env !== '') {
|
|
65
|
-
const n = parseInt(env, 10);
|
|
66
|
-
if (!Number.isNaN(n)) return n;
|
|
67
|
-
}
|
|
68
|
-
if (cfg.buildNumber != null) {
|
|
69
|
-
const n = parseInt(String(cfg.buildNumber), 10);
|
|
70
|
-
if (!Number.isNaN(n)) return n;
|
|
71
|
-
}
|
|
72
|
-
return deriveBuild(cfg.version);
|
|
57
|
+
return resolveBuildNumber(cfg, process.env.APPWRAP_BUILD_NUMBER);
|
|
73
58
|
}
|
|
74
59
|
|
|
75
60
|
const IOS_PERMISSION_KEYS: Record<string, string[]> = {
|
package/src/config.ts
CHANGED
|
@@ -113,8 +113,12 @@ export interface AppwrapConfig {
|
|
|
113
113
|
>;
|
|
114
114
|
/** Monotonic build identifier. Stores reject a re-upload unless this is HIGHER than the last:
|
|
115
115
|
* iOS `CFBundleVersion`, Android `versionCode` (the marketing `version` stays the user-facing
|
|
116
|
-
* string). Default: an integer derived from `version` (0.2.1 → 201). Set
|
|
117
|
-
* run
|
|
116
|
+
* string). Default: an integer derived from `version` (0.2.1 → 201). Set an explicit number from a
|
|
117
|
+
* CI run for fleet builds of one marketing version, OR a named strategy string (resolved by the
|
|
118
|
+
* framework so it can't drift across branches):
|
|
119
|
+
* - `'timestamp'` — YYMMDDHHMM UTC (e.g. 2606221405). iOS-ONLY: exceeds Android's versionCode cap.
|
|
120
|
+
* - `'epoch'` — unix seconds. Android-safe.
|
|
121
|
+
* (`APPWRAP_BUILD_NUMBER` env always wins; an unknown string falls back to the derived default.) */
|
|
118
122
|
buildNumber?: string | number;
|
|
119
123
|
/** iOS export-compliance. `ITSAppUsesNonExemptEncryption` — stamped `false` by default (skips the
|
|
120
124
|
* per-upload prompt). Set `true` only if the app uses non-exempt encryption. */
|
package/src/derive.ts
CHANGED
|
@@ -47,6 +47,56 @@ export function mergeManifest<
|
|
|
47
47
|
return cfg;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
/** Marketing version → a monotonic integer build (0.2.1 → 201; 1.4.12 → 10412). Stable & increasing
|
|
51
|
+
* across semver bumps so store re-uploads are always accepted without a manual bump. */
|
|
52
|
+
export function deriveBuild(version: string): number {
|
|
53
|
+
const [maj = 0, min = 0, patch = 0] = version.split('.').map((n) => parseInt(n, 10) || 0);
|
|
54
|
+
return maj * 10000 + min * 100 + patch;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Named build-number strategies. Live ONCE here so apps opt into a name instead of copying the
|
|
58
|
+
* stamping logic into their config (which drifts across branches). All return a single integer.
|
|
59
|
+
* - `timestamp`: human-readable YYMMDDHHMM (UTC), e.g. 2026-06-22 14:05 → 2606221405. ≈2.6e9, ≤ the
|
|
60
|
+
* iOS CFBundleVersion UInt32 cap (4294967295) until ~2042 — but it EXCEEDS Android versionCode's
|
|
61
|
+
* 2.1e9 cap, so this strategy is iOS-ONLY.
|
|
62
|
+
* - `epoch`: unix seconds (Math.floor(Date.now()/1000)), ≈1.78e9 — Android-safe. */
|
|
63
|
+
const BUILD_STRATEGIES: Record<string, (now: Date) => number> = {
|
|
64
|
+
timestamp: (now) => {
|
|
65
|
+
const p = (n: number) => String(n).padStart(2, '0');
|
|
66
|
+
const yy = now.getUTCFullYear() % 100;
|
|
67
|
+
return Number(
|
|
68
|
+
`${p(yy)}${p(now.getUTCMonth() + 1)}${p(now.getUTCDate())}${p(now.getUTCHours())}${p(now.getUTCMinutes())}`
|
|
69
|
+
);
|
|
70
|
+
},
|
|
71
|
+
epoch: (now) => Math.floor(now.getTime() / 1000),
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Resolve the monotonic build number (iOS CFBundleVersion / Android versionCode). Pure: the CLI
|
|
76
|
+
* passes the `APPWRAP_BUILD_NUMBER` env value in (no process.env read here, so it unit-tests).
|
|
77
|
+
* Precedence: env (CI run #) > explicit numeric `buildNumber` > named strategy > derived from version.
|
|
78
|
+
* An unrecognized strategy string falls back to `deriveBuild` (never crashes).
|
|
79
|
+
*/
|
|
80
|
+
export function resolveBuildNumber(
|
|
81
|
+
cfg: { version: string; buildNumber?: string | number },
|
|
82
|
+
envBuildNumber?: string,
|
|
83
|
+
now: Date = new Date()
|
|
84
|
+
): number {
|
|
85
|
+
if (envBuildNumber != null && envBuildNumber !== '') {
|
|
86
|
+
const n = parseInt(envBuildNumber, 10);
|
|
87
|
+
if (!Number.isNaN(n)) return n;
|
|
88
|
+
}
|
|
89
|
+
if (cfg.buildNumber != null) {
|
|
90
|
+
// A numeric value (number or numeric string) is the literal build number.
|
|
91
|
+
const n = parseInt(String(cfg.buildNumber), 10);
|
|
92
|
+
if (!Number.isNaN(n)) return n;
|
|
93
|
+
// Non-numeric → a strategy NAME. Map it, or fall back to deriveBuild for an unknown string.
|
|
94
|
+
const strategy = BUILD_STRATEGIES[String(cfg.buildNumber).trim()];
|
|
95
|
+
if (strategy) return strategy(now);
|
|
96
|
+
}
|
|
97
|
+
return deriveBuild(cfg.version);
|
|
98
|
+
}
|
|
99
|
+
|
|
50
100
|
/**
|
|
51
101
|
* Collapse a PWA web-manifest `orientation` value to our three states. The manifest spec allows
|
|
52
102
|
* `portrait`/`landscape` plus the `-primary`/`-secondary`/`*-up`/`natural` variants — we don't model
|
|
@@ -46,7 +46,13 @@ platform :ios do
|
|
|
46
46
|
|
|
47
47
|
# Wait for App Store Connect to finish processing and FAIL if it doesn't — Apple can accept the
|
|
48
48
|
# upload yet silently drop the build (e.g. an out-of-range CFBundleVersion). skip:true hides that.
|
|
49
|
-
|
|
49
|
+
# APPWRAP_TF_WAIT_TIMEOUT (seconds) tunes the wait: real builds can exceed 30 min under Apple
|
|
50
|
+
# processing load and crash the lane (BuildWatcher exceeded) — default to 1h, raise it if needed.
|
|
51
|
+
# Guard BOTH unset (nil) and empty/blank env: nil.to_i and "".to_i are 0, which would crash the
|
|
52
|
+
# lane instantly (BuildWatcher exceeded '0'). Only a positive integer overrides the 3600 default.
|
|
53
|
+
tf_wait_env = ENV['APPWRAP_TF_WAIT_TIMEOUT'].to_s.strip
|
|
54
|
+
tf_wait_timeout = tf_wait_env.to_i > 0 ? tf_wait_env.to_i : 3600
|
|
55
|
+
upload_to_testflight(api_key: api_key, skip_waiting_for_build_processing: false, wait_processing_timeout_duration: tf_wait_timeout)
|
|
50
56
|
end
|
|
51
57
|
end
|
|
52
58
|
|