@livx.cc/native-kit 0.39.0 → 0.41.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livx.cc/native-kit",
3
- "version": "0.39.0",
3
+ "version": "0.41.0",
4
4
  "description": "Isomorphic native-capabilities kit for PWAs — same API in browser and in an appwrap native shell. Zero dependencies.",
5
5
  "license": "MIT",
6
6
  "author": "Elya Livshitz",
@@ -43,6 +43,22 @@ export class NativeKitOptions {
43
43
  handshakeTimeoutMs = 3000;
44
44
  }
45
45
 
46
+ /**
47
+ * Method domains the web fallback must NEVER be retried against — deliberately NOT an option:
48
+ * this is a policy invariant, not a knob a consumer should be able to switch off.
49
+ *
50
+ * `billing.`: purchases inside a native shell belong to the device store, full stop.
51
+ * - {@link BillingModule.capability} promises to "never silently fall back to web inside a
52
+ * native shell" — the generic retry here would bypass that contract behind its back.
53
+ * - The WebAdapter's only billing behaviour is a hardcoded throw ("use a web checkout"), so
54
+ * retrying billing can't succeed — it can only REPLACE the shell's real, actionable error
55
+ * (e.g. UNSUPPORTED "Purchases are disabled on this device" from a Screen-Time/MDM-restricted
56
+ * iPhone) with a false one claiming the user is on the web. Zero upside, only downside.
57
+ * - Steering an iOS user to an external checkout is App Store Guideline 3.1.1 territory. Today
58
+ * that string only reaches a `catch`; this keeps it from ever reaching a paywall.
59
+ */
60
+ const WEB_FALLBACK_EXCLUDED_DOMAINS = ['billing.'];
61
+
46
62
  /**
47
63
  * Flat, vendor-neutral analytics bag — spread straight into your analytics provider's
48
64
  * super-properties (e.g. `mixpanel.register(await kit.context())`). Native-only fields
@@ -265,7 +281,12 @@ export class NativeKit {
265
281
  try {
266
282
  return await this.adapter!.invoke<T>(method, params, opts);
267
283
  } catch (e) {
268
- if (this.webFallback && e instanceof KitError && e.code === 'UNSUPPORTED') {
284
+ if (
285
+ this.webFallback &&
286
+ e instanceof KitError &&
287
+ e.code === 'UNSUPPORTED' &&
288
+ !WEB_FALLBACK_EXCLUDED_DOMAINS.some((d) => method.startsWith(d))
289
+ ) {
269
290
  return this.webFallback.invoke<T>(method, params, opts);
270
291
  }
271
292
  throw e;
@@ -332,9 +332,15 @@ export class WebAdapter implements NativeKitAdapter {
332
332
  last = now;
333
333
  const a = e.accelerationIncludingGravity;
334
334
  const r = e.rotationRate;
335
+ // Map DOM → kit contract. Two corrections vs the raw event, BOTH load-bearing:
336
+ // 1. UNITS: the DOM reports rotationRate in deg/s; the kit contract (and both native
337
+ // handlers — CoreMotion + Android TYPE_GYROSCOPE) is rad/s. Convert, or web is 57× off.
338
+ // 2. AXES: DOM alpha/beta/gamma are the rates about Z/X/Y respectively — NOT x/y/z in
339
+ // order. So rx=beta, ry=gamma, rz=alpha; a naive alpha→rx puts the Z-rate on X.
340
+ const toRad = (d?: number | null) => (d == null ? undefined : (d * Math.PI) / 180);
335
341
  this.emit('motion.data', {
336
342
  ax: a?.x ?? 0, ay: a?.y ?? 0, az: a?.z ?? 0,
337
- rx: r?.alpha ?? undefined, ry: r?.beta ?? undefined, rz: r?.gamma ?? undefined,
343
+ rx: toRad(r?.beta), ry: toRad(r?.gamma), rz: toRad(r?.alpha),
338
344
  });
339
345
  };
340
346
  window.addEventListener('devicemotion', this.motionHandler);
@@ -116,7 +116,7 @@ export class BillingModule {
116
116
  */
117
117
  async receiptEntitlements(): Promise<ReceiptEntitlements> {
118
118
  await this.kit.ready();
119
- if (this.capability !== 'native') return { entitlements: [], outcome: 'not-native' }; // web, or a shell with no native store handler (Android today)
119
+ if (this.capability !== 'native') return { entitlements: [], outcome: 'not-native' }; // web, or a shell with no native store handler (Android IS native now, but has no on-disk receipt → yields 'no-receipt' below)
120
120
  if (this.validator instanceof ClientTrustedValidator) return { entitlements: [], outcome: 'no-validator' }; // bare receipt has no product id → would grant a bogus active entitlement; needs a server validator
121
121
  const receipt = await this.kit.invoke<PurchaseReceipt>('billing.appReceipt');
122
122
  if (!receipt?.appReceipt) return { entitlements: [], outcome: 'no-receipt' };
@@ -142,19 +142,22 @@ export class BillingModule {
142
142
  }
143
143
 
144
144
  /**
145
- * iOS 15+ native-only: present StoreKit 2's **in-app** subscription-management sheet
146
- * (`AppStore.showManageSubscriptions(in:)`). Unlike {@link manageSubscriptions} (which deep-links
147
- * out to the App Store account page, StoreKit 1), this stays inside the app AND shows sandbox /
148
- * TestFlight subscriptions — so it's the path to verify/cancel a sub during testing. Opt-in: the
149
- * default `manageSubscriptions()` deep-link is unchanged; call this explicitly to use the sheet.
145
+ * The in-app / native subscription-management surface. On iOS 15+ this presents StoreKit 2's
146
+ * **in-app** sheet (`AppStore.showManageSubscriptions(in:)`) — unlike {@link manageSubscriptions}
147
+ * (App Store account page, StoreKit 1), it stays inside the app AND shows sandbox / TestFlight
148
+ * subscriptions, so it's the path to verify/cancel a sub during testing. On Android (native billing
149
+ * enabled) there is no in-app sheet, so this dispatches to the same Play subscriptions deep link as
150
+ * {@link manageSubscriptions} (the native handler maps both). Opt-in on iOS: the default
151
+ * `manageSubscriptions()` deep-link is unchanged; call this explicitly to use the sheet.
150
152
  *
151
- * Throws `UNSUPPORTED` on the web or Android (no StoreKit), and `NATIVE_ERROR` on iOS <15 / no
152
- * presentable scene / a StoreKit error. Resolves once the user dismisses the sheet.
153
+ * Throws `UNSUPPORTED` only on the web (no native store), and `NATIVE_ERROR` on iOS <15 / no
154
+ * presentable scene / a StoreKit error. Resolves once the user dismisses the sheet (iOS) or the
155
+ * deep link is opened (Android).
153
156
  */
154
157
  async showManageSubscriptionsSheet(): Promise<void> {
155
158
  await this.kit.ready();
156
159
  if (this.capability !== 'native') {
157
- throw new KitError('UNSUPPORTED', 'The in-app subscriptions sheet is iOS-only (StoreKit 2). Use manageSubscriptions() on web/Android.');
160
+ throw new KitError('UNSUPPORTED', 'The subscriptions sheet needs a native store. On the web, use manageSubscriptions() (your web billing provider portal).');
158
161
  }
159
162
  // Dismiss-bound: resolves only when the USER closes the sheet (no meaningful deadline).
160
163
  // A watchdog could only false-timeout mid-sheet → spurious fallback, so disable it.
@@ -8,6 +8,18 @@ export type ProductType =
8
8
  | 'nonRenewable'
9
9
  | 'unknown';
10
10
 
11
+ /** An introductory / free-trial phase that precedes the standard subscription price. */
12
+ export interface IntroOffer {
13
+ /** Price during the intro phase in `currency`; 0 for a free trial. */
14
+ price: number;
15
+ /** Localized formatted intro price, e.g. "Free" / "$0.99" — show this to users. */
16
+ displayPrice: string;
17
+ /** ISO-8601 duration of the intro phase, e.g. "P1W", "P7D". */
18
+ period?: string;
19
+ /** True when `price === 0` (a free trial). */
20
+ isFreeTrial: boolean;
21
+ }
22
+
11
23
  /** A purchasable product as the store describes it (localized). */
12
24
  export interface Product {
13
25
  id: string;
@@ -21,6 +33,13 @@ export interface Product {
21
33
  type: ProductType;
22
34
  /** ISO-8601 duration for auto-renewables, e.g. "P1M", "P1Y". */
23
35
  subscriptionPeriod?: string;
36
+ /**
37
+ * Introductory / free-trial offer the CURRENT user is eligible for, when the store reports one.
38
+ * Populated on Android (Play Billing SubscriptionOfferDetails' zero-price phase). Not yet populated
39
+ * on iOS — StoreKit 1's `introductoryPrice` is unread by the current iOS mapper (parity follow-up);
40
+ * the trial still APPLIES at checkout because StoreKit/Play show trial terms in their own sheet.
41
+ */
42
+ introOffer?: IntroOffer;
24
43
  }
25
44
 
26
45
  /** A normalized entitlement — "this user owns/subscribes to X". */
@@ -65,7 +84,9 @@ export interface PurchaseResult {
65
84
  * - `validated` the server-of-record read the receipt; `entitlements` is its verdict
66
85
  * (still `[]` when the user genuinely owns nothing — the only outcome
67
86
  * where an empty list is an ANSWER rather than a non-answer).
68
- * - `not-native` web, or a shell with no native store handler (Android: no StoreKit-1 receipt).
87
+ * - `not-native` web, or a shell with no native store handler. (Android now HAS a native billing
88
+ * handler, so it reports `native`/`no-receipt` here — not `not-native`; it just has
89
+ * no StoreKit-1-style on-disk app receipt, so `receiptEntitlements` yields `no-receipt`.)
69
90
  * - `no-validator` only a {@link ClientTrustedValidator} is wired; a bare app receipt carries no
70
91
  * product id, so trusting it would grant a bogus entitlement. Wire a server validator.
71
92
  * - `no-receipt` this build has no App Store receipt. StoreKit only issues one after a purchase or