@livx.cc/native-kit 0.42.0 → 0.44.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.42.0",
3
+ "version": "0.44.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",
@@ -5,6 +5,7 @@ import type { KitModuleRegistry } from './module-registry';
5
5
  import { Capability, Handshake, InvokeOptions, KIT_PROTOCOL, KitError, NativeKitAdapter, Platform, Unsubscribe } from './types';
6
6
  import { AppModule } from '../modules/app';
7
7
  import { AppleSignInModule } from '../modules/appleSignIn';
8
+ import { ShareTargetModule } from '../modules/shareTarget';
8
9
  import { BackgroundTaskModule } from '../modules/backgroundTask';
9
10
  import { BiometricsModule } from '../modules/biometrics';
10
11
  import { BrowserModule } from '../modules/browser';
@@ -107,6 +108,7 @@ export class NativeKit {
107
108
  public readonly backgroundTask = new BackgroundTaskModule(this);
108
109
  public readonly tracking = new TrackingModule(this);
109
110
  public readonly appleSignIn = new AppleSignInModule(this);
111
+ public readonly shareTarget = new ShareTargetModule(this);
110
112
 
111
113
  /**
112
114
  * Additive, string-keyed registry mirroring the eager fields above (SAME instances) — a new
@@ -152,6 +154,7 @@ export class NativeKit {
152
154
  speech: this.speech, calendar: this.calendar, app: this.app, browser: this.browser,
153
155
  oauth: this.oauth, updates: this.updates,
154
156
  backgroundTask: this.backgroundTask, tracking: this.tracking, appleSignIn: this.appleSignIn,
157
+ shareTarget: this.shareTarget,
155
158
  };
156
159
  for (const [name, inst] of Object.entries(eager)) this.modules.registerModule(name, inst);
157
160
  }
@@ -378,6 +381,7 @@ declare module './module-registry' {
378
381
  backgroundTask: BackgroundTaskModule;
379
382
  tracking: TrackingModule;
380
383
  appleSignIn: AppleSignInModule;
384
+ shareTarget: ShareTargetModule;
381
385
  }
382
386
  }
383
387
 
@@ -43,8 +43,14 @@ export class ModuleRegistry {
43
43
  return [...new Set([...this.instances.keys(), ...this.factories.keys()])].sort();
44
44
  }
45
45
 
46
+ /**
47
+ * Typed, string-keyed lookup. STRICT by design: the ONLY public signature is keyed to
48
+ * `keyof KitModuleRegistry`, so `getModule('billing')` is a COMPILE ERROR unless the billing pack's
49
+ * kit client has been imported (its `declare module` augments the registry) — you get compile-time
50
+ * verification + autocomplete of exactly the modules you've wired, never a runtime surprise. For a
51
+ * genuinely dynamic name, use {@link getModuleUnsafe}.
52
+ */
46
53
  getModule<K extends keyof KitModuleRegistry>(name: K): KitModuleRegistry[K];
47
- getModule<T = unknown>(name: string): T;
48
54
  getModule(name: string): unknown {
49
55
  if (this.instances.has(name)) return this.instances.get(name);
50
56
  const factory = this.factories.get(name);
@@ -61,4 +67,10 @@ export class ModuleRegistry {
61
67
  `Did you list its pack in modulePacks?`
62
68
  );
63
69
  }
70
+
71
+ /** Escape hatch for a genuinely DYNAMIC module name (not a compile-time literal) — bypasses the
72
+ * keyed check. Prefer {@link getModule}; reach for this only when the name isn't statically known. */
73
+ getModuleUnsafe<T = unknown>(name: string): T {
74
+ return (this.getModule as (n: string) => unknown)(name) as T;
75
+ }
64
76
  }
package/src/index.ts CHANGED
@@ -53,4 +53,6 @@ export type {
53
53
  } from './modules/appleSignIn';
54
54
  export { isAppleSignInResult } from './modules/appleSignIn';
55
55
  export { BackgroundTaskModule } from './modules/backgroundTask';
56
+ export { ShareTargetModule, parseSharePayload } from './modules/shareTarget';
57
+ export type { SharedPayload } from './modules/shareTarget';
56
58
  export type { BackgroundTaskHandler, ScheduleBackgroundTaskOptions } from './modules/backgroundTask';
@@ -0,0 +1,93 @@
1
+ import type { NativeKit } from '../core/NativeKit';
2
+ import type { Unsubscribe } from '../core/types';
3
+
4
+ /** A payload shared INTO the app from the OS share sheet. */
5
+ export interface SharedPayload {
6
+ /** Shared text / URL (Android EXTRA_TEXT). */
7
+ text?: string;
8
+ /** Share subject/title (Android EXTRA_SUBJECT), when the sender set one. */
9
+ title?: string;
10
+ /** Shared files (images), copied by the shell into the app cache — each entry is a path relative
11
+ * to the cache root, readable via `kit.fs.read(p, { dir: 'cache', encoding: 'base64' })`. */
12
+ files?: string[];
13
+ }
14
+
15
+ /**
16
+ * Parse a shareTarget deep link (`<scheme>://share?text=…&title=…&file=…&file=…`) into a payload.
17
+ * Returns null for any URL that isn't a share delivery — safe to feed every deep link through.
18
+ */
19
+ export function parseSharePayload(url: string | null | undefined): SharedPayload | null {
20
+ // Custom-scheme `<scheme>://share?…` only — http(s) URLs are ordinary web links, never a share delivery.
21
+ if (!url || !/^(?!https?:)[a-z][a-z0-9.+-]*:\/\/share(\?|$)/i.test(url)) return null;
22
+ const q = new URLSearchParams(url.split('?')[1] ?? '');
23
+ const payload: SharedPayload = {};
24
+ const text = q.get('text');
25
+ if (text) payload.text = text;
26
+ const title = q.get('title');
27
+ if (title) payload.title = title;
28
+ const files = q.getAll('file');
29
+ if (files.length) payload.files = files;
30
+ return Object.keys(payload).length ? payload : null;
31
+ }
32
+
33
+ /**
34
+ * Inbound share-target (`shareTarget` module — opt-in): receive content shared TO the app from the
35
+ * OS share sheet. Delivery rides the deep-link path — the shell synthesizes
36
+ * `<urlScheme>://share?text=…&title=…&file=…` for both cold launches (handshake deep link) and warm
37
+ * shares (deeplink.open) — so this module is a thin typed parser over `kit.lifecycle`.
38
+ *
39
+ * Capability (honest): Android + iOS `'native'` — Android via an ACTION_SEND intent-filter on the
40
+ * main activity, iOS via a generated `AppwrapShare` share-extension target that forwards over the
41
+ * app's `urlScheme` (config REQUIRED) with images crossing through the App Group container; web
42
+ * `'none'` (no Web Share Target wiring).
43
+ */
44
+ export class ShareTargetModule {
45
+ private launchConsumed = false;
46
+
47
+ constructor(private kit: NativeKit) {}
48
+
49
+ get capability() {
50
+ return this.kit.capability('shareTarget');
51
+ }
52
+
53
+ /**
54
+ * Subscribe to inbound shares. Fires for warm shares immediately; the COLD-START share (the app was
55
+ * launched by the share) is replayed once to the first subscriber after `kit.ready()` resolves —
56
+ * subscribe early (right after boot) to catch it.
57
+ */
58
+ /**
59
+ * Publish the app's SHARE CONTEXT — a small string KV the iOS share extension can read (App Group
60
+ * `appwrap-share-context`) to complete a share directly against the app's backend (the config's
61
+ * `shareTarget.directSync` lane resolves its `{key}` template placeholders from this KV). The
62
+ * framework treats the KV as opaque — publish whatever your sync endpoint needs (e.g. `{ binId }`)
63
+ * and re-publish whenever it changes. `null` clears it (direct sync falls back to the mailbox).
64
+ * Rejects (`UNSUPPORTED`) on the web / on shells that predate the method — treat it as
65
+ * fire-and-forget with a `.catch` (the mailbox path needs no context).
66
+ */
67
+ setContext(context: Record<string, string | number> | null): Promise<void> {
68
+ return this.kit.invoke('shareTarget.setContext', { context });
69
+ }
70
+
71
+ onReceive(cb: (payload: SharedPayload) => void): Unsubscribe {
72
+ let cancelled = false;
73
+ void this.kit
74
+ .ready()
75
+ .then(() => {
76
+ if (cancelled || this.launchConsumed) return;
77
+ const payload = parseSharePayload(this.kit.handshakeInfo?.deepLink);
78
+ if (payload) {
79
+ this.launchConsumed = true; // read-once: a second subscriber shouldn't re-ingest it
80
+ cb(payload);
81
+ }
82
+ })
83
+ .catch(() => { /* web/no-shell: no handshake, nothing to replay */ });
84
+ const un = this.kit.on('deeplink.open', (p) => {
85
+ const payload = parseSharePayload((p as { url: string }).url);
86
+ if (payload) cb(payload);
87
+ });
88
+ return () => {
89
+ cancelled = true;
90
+ un();
91
+ };
92
+ }
93
+ }