@livx.cc/native-kit 0.32.3 → 0.35.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/README.md CHANGED
@@ -26,7 +26,7 @@ mixpanel.register(await kit.context());
26
26
 
27
27
  The kit detects whether it's running inside the appwrap shell and routes each call to the native bridge or a web fallback, so a single web build works everywhere. Capabilities advertise their real backing (`native` / `web` / `none`) so your UI can be honest about what's available.
28
28
 
29
- Covers haptics, share, storage, notifications, OAuth, billing (IAP), health, geo, camera, photos, contacts, calendar, motion, reviews, and more.
29
+ Covers haptics, share, storage, notifications, OAuth, billing (IAP), health, geo, camera, photos, contacts, calendar, motion, reviews, tracking (iOS App Tracking Transparency), and more.
30
30
 
31
31
  **Full documentation, capability model, and the bridge protocol:** see the [appwrap README](https://github.com/Livshitz/appwrap#readme).
32
32
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@livx.cc/native-kit",
3
- "version": "0.32.3",
3
+ "version": "0.35.0",
4
4
  "description": "Isomorphic native-capabilities kit for PWAs \u2014 same API in browser and in an appwrap native shell. Zero dependencies.",
5
5
  "license": "MIT",
6
6
  "author": "Elya Livshitz",
@@ -2,6 +2,7 @@ import { AppwrapAdapter } from './appwrap-adapter';
2
2
  import { WebAdapter } from './web-adapter';
3
3
  import { Capability, Handshake, InvokeOptions, KIT_PROTOCOL, KitError, NativeKitAdapter, Platform, Unsubscribe } from './types';
4
4
  import { AppModule } from '../modules/app';
5
+ import { AppleSignInModule } from '../modules/appleSignIn';
5
6
  import { BackgroundTaskModule } from '../modules/backgroundTask';
6
7
  import { BillingModule } from '../modules/billing/billing';
7
8
  import { BiometricsModule } from '../modules/biometrics';
@@ -30,6 +31,7 @@ import { ShareModule } from '../modules/share';
30
31
  import { SpeechModule } from '../modules/speech';
31
32
  import { StorageModule } from '../modules/storage';
32
33
  import { ToastModule } from '../modules/toast';
34
+ import { TrackingModule } from '../modules/tracking';
33
35
  import { UiModule } from '../modules/ui';
34
36
  import { UpdatesModule } from '../modules/updates';
35
37
 
@@ -103,6 +105,8 @@ export class NativeKit {
103
105
  public readonly billing = new BillingModule(this);
104
106
  public readonly updates = new UpdatesModule(this);
105
107
  public readonly backgroundTask = new BackgroundTaskModule(this);
108
+ public readonly tracking = new TrackingModule(this);
109
+ public readonly appleSignIn = new AppleSignInModule(this);
106
110
 
107
111
  public handshakeInfo: Handshake | null = null;
108
112
  public options: NativeKitOptions;
package/src/core/types.ts CHANGED
@@ -29,6 +29,12 @@ export interface Handshake {
29
29
  * reads it from {@link NativeKit.handshakeInfo} and dispatches the registered handler. Absent on a
30
30
  * normal foreground launch. See {@link BackgroundTaskModule}. */
31
31
  backgroundTaskId?: string;
32
+ /** Set ONLY on a cold launch FROM a deep link (url-scheme open or notification tap that started the
33
+ * app): the link that launched us, handed back synchronously so the PWA can route to the target
34
+ * route BEFORE first paint — avoiding a brief home-screen flash. A WARM deep link (app already
35
+ * running) is NOT carried here; it arrives via the `deeplink.open` event. Read it at `ready()` via
36
+ * {@link LifecycleModule.launchDeepLink}. Absent on a normal launch. */
37
+ deepLink?: string;
32
38
  /** Optional diagnostic payload (breadcrumbs, etc.). */
33
39
  debug?: Record<string, unknown>;
34
40
  }
package/src/index.ts CHANGED
@@ -41,6 +41,14 @@ export type { SpeechVoice, SpeakOptions, ListenOptions, SpeechPartial } from './
41
41
  export type { CalendarEventOptions } from './modules/calendar';
42
42
  export type { BrowserOptions } from './modules/browser';
43
43
  export type { OAuthAuthorizeParams, OAuthResult } from './modules/oauth';
44
+ export type { TrackingStatus } from './modules/tracking';
45
+ export type {
46
+ AppleSignInName,
47
+ AppleSignInParams,
48
+ AppleSignInResult,
49
+ AppleSignInCancelled,
50
+ } from './modules/appleSignIn';
51
+ export { isAppleSignInResult } from './modules/appleSignIn';
44
52
  export { ClientTrustedValidator, HttpValidator, HttpValidatorOptions } from './modules/billing/validators';
45
53
  export { HttpBillingProvider, HttpBillingProviderOptions } from './modules/billing/providers';
46
54
  export type { HeaderProvider } from './modules/billing/http';
@@ -0,0 +1,96 @@
1
+ import type { NativeKit } from '../core/NativeKit';
2
+ import { KitError } from '../core/types';
3
+
4
+ /** The name Apple returns on the FIRST authorization per Apple ID (subsequent sign-ins omit it). */
5
+ export interface AppleSignInName {
6
+ /** Given (first) name, when provided. */
7
+ givenName?: string;
8
+ /** Family (last) name, when provided. */
9
+ familyName?: string;
10
+ /** A display name composed by the OS from the components, when provided. */
11
+ displayName?: string;
12
+ }
13
+
14
+ /** A successful native Sign in with Apple. Feed `identityToken` + `nonce` (raw) to Firebase:
15
+ * `signInWithCredential(OAuthProvider.credential('apple.com', { idToken: identityToken, rawNonce: nonce }))`. */
16
+ export interface AppleSignInResult {
17
+ /** The Apple identity JWT (`id_token`) — what Firebase / your backend verifies. */
18
+ identityToken: string;
19
+ /** Short-lived authorization code (for an optional server-side token exchange). */
20
+ authorizationCode?: string;
21
+ /** The RAW nonce you passed in — Apple hashed SHA256(nonce); Firebase needs the raw value back. */
22
+ nonce: string;
23
+ /** First-authorization-only profile. Apple returns name/email ONLY the first time per Apple ID —
24
+ * persist it on first sign-in; it is absent on every subsequent call. */
25
+ user?: { name?: AppleSignInName; email?: string };
26
+ }
27
+
28
+ /** Sign in resolved without a credential because the user dismissed the system sheet. */
29
+ export interface AppleSignInCancelled {
30
+ cancelled: true;
31
+ }
32
+
33
+ export interface AppleSignInParams {
34
+ /** A RAW, cryptographically-random nonce (the caller generates it). The handler sends SHA256(nonce)
35
+ * to Apple and returns this raw value so you can pass it to Firebase as `rawNonce`. Required. */
36
+ nonce: string;
37
+ /** Profile scopes to request on first authorization. Default `['name', 'email']`. */
38
+ scopes?: Array<'name' | 'email'>;
39
+ }
40
+
41
+ /** True when an {@link AppleSignInResult} (a credential) is present (not the cancelled shape). */
42
+ export function isAppleSignInResult(r: AppleSignInResult | AppleSignInCancelled): r is AppleSignInResult {
43
+ return (r as AppleSignInCancelled).cancelled !== true;
44
+ }
45
+
46
+ /**
47
+ * Native Sign in with Apple — iOS `ASAuthorizationController` (the system account sheet). Returns the
48
+ * `identityToken` (JWT) + the raw nonce DIRECTLY to JS, so a wrapped app does true-native Apple auth and
49
+ * hands the result to Firebase `signInWithCredential(OAuthProvider.credential('apple.com', { idToken,
50
+ * rawNonce }))` — no Services ID, no https Return URL, no browser redirect, no cross-origin storage.
51
+ *
52
+ * WHY (not `kit.oauth`): the web-OAuth path (`ASWebAuthenticationSession`) needs an Apple **Services ID**
53
+ * with a custom-scheme `redirect_uri`, which Apple rejects (Services IDs require registered https Return
54
+ * URLs). The native ASAuthorization flow uses the **App ID** (bundle) and returns the token directly.
55
+ *
56
+ * Provider-agnostic, like `kit.push`/`kit.oauth`: the shell owns only the device-side primitive (present
57
+ * the sheet, hash the nonce, hand back the token). The PWA generates the nonce and does the Firebase
58
+ * `signInWithCredential` exchange.
59
+ *
60
+ * Capability gating (honest):
61
+ * - iOS 13+: `'native'`.
62
+ * - Android / web / iOS < 13: `'none'` — Sign in with Apple has NO native SDK off iOS. {@link signIn}
63
+ * throws `UNSUPPORTED`; the PWA should fall back to its web Apple auth (Firebase popup/redirect)
64
+ * when `capability !== 'native'`. Branch on {@link capability}.
65
+ *
66
+ * Cancel contract: a user-dismissed sheet RESOLVES `{ cancelled: true }` (never throws) — mirrors
67
+ * {@link import('./scanner').ScannerModule}. Real failures reject with a {@link import('../core/types').KitError}.
68
+ */
69
+ export class AppleSignInModule {
70
+ constructor(private kit: NativeKit) {}
71
+
72
+ /** `'native'` on an iOS shell · `'none'` on Android/web/iOS<13. */
73
+ get capability() {
74
+ return this.kit.capability('appleSignIn');
75
+ }
76
+
77
+ /**
78
+ * Present the native Sign in with Apple sheet. Resolves an {@link AppleSignInResult} on success or
79
+ * `{ cancelled: true }` if the user dismisses it (branch with {@link isAppleSignInResult}).
80
+ *
81
+ * The handler hashes the supplied raw `nonce` (SHA256) before sending it to Apple and returns the
82
+ * RAW nonce in the result so you can pass it to Firebase as `rawNonce`.
83
+ *
84
+ * Dismiss-bound — the sheet resolves only when the user completes or cancels, so there's no watchdog
85
+ * timeout (a deadline would abandon the request mid-handshake). Throws `UNSUPPORTED` off iOS.
86
+ */
87
+ signIn(params: AppleSignInParams): Promise<AppleSignInResult | AppleSignInCancelled> {
88
+ const nonce = String(params?.nonce ?? '');
89
+ if (!nonce) return Promise.reject(new KitError('NATIVE_ERROR', 'appleSignIn: a raw nonce is required'));
90
+ if (this.capability !== 'native') {
91
+ return Promise.reject(new KitError('UNSUPPORTED', 'appleSignIn is iOS-only (no native Sign in with Apple off iOS)'));
92
+ }
93
+ const scopes = params.scopes ?? ['name', 'email'];
94
+ return this.kit.invoke('appleSignIn.signIn', { nonce, scopes }, { timeoutMs: 'none' });
95
+ }
96
+ }
@@ -20,4 +20,15 @@ export class LifecycleModule {
20
20
  onDeepLink(cb: (url: string) => void): Unsubscribe {
21
21
  return this.kit.on('deeplink.open', (p) => cb((p as { url: string }).url));
22
22
  }
23
+
24
+ /**
25
+ * The deep link the app was COLD-LAUNCHED from (url-scheme open / notification tap that started the
26
+ * app), if any — exposed synchronously from the handshake so the app can route to the target route
27
+ * BEFORE first paint and skip the brief home flash. Returns null on a normal launch or on web.
28
+ * Available once `kit.ready()` has resolved. Warm deep links (app already open) do NOT appear here —
29
+ * subscribe to {@link onDeepLink} for those.
30
+ */
31
+ get launchDeepLink(): string | null {
32
+ return this.kit.handshakeInfo?.deepLink ?? null;
33
+ }
23
34
  }
@@ -0,0 +1,73 @@
1
+ import type { NativeKit } from '../core/NativeKit';
2
+
3
+ /** App Tracking Transparency authorization status (iOS ATTrackingManager). Mirrors Apple's enum.
4
+ * - `notDetermined` — the user hasn't been asked yet (call {@link TrackingModule.requestPermission}).
5
+ * - `restricted` — tracking is disallowed by device policy (e.g. parental controls / MDM).
6
+ * - `denied` — the user declined; you MUST NOT track / read the IDFA.
7
+ * - `authorized` — the user allowed tracking; {@link TrackingModule.idfa} returns the IDFA. */
8
+ export type TrackingStatus = 'notDetermined' | 'restricted' | 'denied' | 'authorized';
9
+
10
+ /**
11
+ * App Tracking Transparency (iOS, `tracking` module — opt-in). The native-only compliance seam for
12
+ * apps that track the user across other companies' apps/sites (IDFA, cross-app identity): Apple
13
+ * REQUIRES the ATT prompt + `NSUserTrackingUsageDescription` and forbids tracking before consent.
14
+ *
15
+ * Provider-agnostic, like `kit.push`/`kit.oauth`: the shell owns only the device-side primitive
16
+ * (show the prompt, report status, hand back the IDFA when authorized). What you DO with consent —
17
+ * init an ad SDK, set an analytics super-prop — is the PWA's job.
18
+ *
19
+ * Capability gating (honest):
20
+ * - iOS (14.5+): `'native'`.
21
+ * - Android / web / iOS < 14.5: `'none'` — there is NO ATT. The methods still resolve safely:
22
+ * `requestPermission()`/`status()` → `'authorized'` (no consent gate exists, so tracking is not
23
+ * blocked by the OS — Play's Advertising-ID consent is a separate, app-declared concern), and
24
+ * `idfa()` → `undefined` (the IDFA is iOS-only; GAID needs the AD_ID permission + Play's own flow).
25
+ *
26
+ * Most apps need only first-party analytics and should NOT enable this module (no string = Apple
27
+ * assumes no tracking). Ship it only when the app genuinely tracks across companies.
28
+ */
29
+ export class TrackingModule {
30
+ constructor(private kit: NativeKit) {}
31
+
32
+ get capability() {
33
+ return this.kit.capability('tracking');
34
+ }
35
+
36
+ /** Honest fallback when ATT doesn't exist (Android/web/iOS<14.5): nothing gates tracking at the OS
37
+ * level, so report `authorized` rather than a misleading `notDetermined`. */
38
+ private get fallback(): TrackingStatus {
39
+ return 'authorized';
40
+ }
41
+
42
+ /**
43
+ * Show the iOS ATT prompt and resolve with the resulting status. The prompt only appears once per
44
+ * install while status is `notDetermined`; subsequent calls resolve immediately with the prior
45
+ * choice. Dismiss-bound (the user decides at their leisure) → no watchdog timeout.
46
+ * On a non-ATT platform resolves to {@link fallback} without showing UI.
47
+ */
48
+ requestPermission(): Promise<TrackingStatus> {
49
+ if (this.capability !== 'native') return Promise.resolve(this.fallback);
50
+ return this.kit.invoke<TrackingStatus>('tracking.requestPermission', undefined, { timeoutMs: 'none' });
51
+ }
52
+
53
+ /** Current ATT authorization status WITHOUT prompting. {@link fallback} on a non-ATT platform. */
54
+ status(): Promise<TrackingStatus> {
55
+ if (this.capability !== 'native') return Promise.resolve(this.fallback);
56
+ return this.kit.invoke<TrackingStatus>('tracking.status');
57
+ }
58
+
59
+ /** Alias of {@link status} (parity with other permissioned modules' `permissionStatus`). */
60
+ permissionStatus(): Promise<TrackingStatus> {
61
+ return this.status();
62
+ }
63
+
64
+ /**
65
+ * The IDFA (iOS advertising identifier) — returned ONLY while ATT status is `authorized`; otherwise
66
+ * (denied/restricted/notDetermined, or the all-zero placeholder) resolves `undefined`. Always
67
+ * `undefined` off iOS (no IDFA exists — use Play's Advertising ID via your own flow if needed).
68
+ */
69
+ idfa(): Promise<string | undefined> {
70
+ if (this.capability !== 'native') return Promise.resolve(undefined);
71
+ return this.kit.invoke<string | undefined>('tracking.idfa');
72
+ }
73
+ }