@livx.cc/native-kit 0.16.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 +27 -0
- package/src/core/NativeKit.ts +131 -0
- package/src/core/appwrap-adapter.ts +107 -0
- package/src/core/types.ts +78 -0
- package/src/core/web-adapter.ts +487 -0
- package/src/index.ts +48 -0
- package/src/modules/app.ts +20 -0
- package/src/modules/billing/billing.ts +109 -0
- package/src/modules/billing/http.ts +28 -0
- package/src/modules/billing/providers.ts +68 -0
- package/src/modules/billing/types.ts +93 -0
- package/src/modules/billing/validators.ts +66 -0
- package/src/modules/biometrics.ts +19 -0
- package/src/modules/browser.ts +22 -0
- package/src/modules/calendar.ts +23 -0
- package/src/modules/clipboard.ts +17 -0
- package/src/modules/contacts.ts +22 -0
- package/src/modules/device.ts +23 -0
- package/src/modules/geo.ts +41 -0
- package/src/modules/haptics.ts +20 -0
- package/src/modules/health.ts +70 -0
- package/src/modules/lifecycle.ts +23 -0
- package/src/modules/media.ts +75 -0
- package/src/modules/motion.ts +41 -0
- package/src/modules/network.ts +24 -0
- package/src/modules/notifications.ts +45 -0
- package/src/modules/oauth.ts +41 -0
- package/src/modules/photos.ts +39 -0
- package/src/modules/push.ts +71 -0
- package/src/modules/reviews.ts +15 -0
- package/src/modules/screen.ts +55 -0
- package/src/modules/share.ts +37 -0
- package/src/modules/storage.ts +55 -0
- package/src/modules/toast.ts +13 -0
- package/src/modules/ui.ts +135 -0
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@livx.cc/native-kit",
|
|
3
|
+
"version": "0.16.0",
|
|
4
|
+
"description": "Isomorphic native-capabilities kit for PWAs — same API in browser and in an appwrap native shell. Zero dependencies.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.ts",
|
|
7
|
+
"module": "./src/index.ts",
|
|
8
|
+
"types": "./src/index.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./src/index.ts",
|
|
12
|
+
"import": "./src/index.ts"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"src"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "bun build src/index.ts --outdir dist --format esm && bun build src/index.ts --outfile dist/native-kit.iife.js --format iife --global-name NativeKitLib"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"typescript": "^5.4.0"
|
|
23
|
+
},
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public"
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { AppwrapAdapter } from './appwrap-adapter';
|
|
2
|
+
import { WebAdapter } from './web-adapter';
|
|
3
|
+
import { Capability, Handshake, InvokeOptions, KIT_PROTOCOL, KitError, NativeKitAdapter, Unsubscribe } from './types';
|
|
4
|
+
import { AppModule } from '../modules/app';
|
|
5
|
+
import { BillingModule } from '../modules/billing/billing';
|
|
6
|
+
import { BiometricsModule } from '../modules/biometrics';
|
|
7
|
+
import { BrowserModule } from '../modules/browser';
|
|
8
|
+
import { CalendarModule } from '../modules/calendar';
|
|
9
|
+
import { ClipboardModule } from '../modules/clipboard';
|
|
10
|
+
import { ContactsModule } from '../modules/contacts';
|
|
11
|
+
import { DeviceModule } from '../modules/device';
|
|
12
|
+
import { GeoModule } from '../modules/geo';
|
|
13
|
+
import { HapticsModule } from '../modules/haptics';
|
|
14
|
+
import { HealthModule } from '../modules/health';
|
|
15
|
+
import { LifecycleModule } from '../modules/lifecycle';
|
|
16
|
+
import { MediaModule } from '../modules/media';
|
|
17
|
+
import { MotionModule } from '../modules/motion';
|
|
18
|
+
import { NetworkModule } from '../modules/network';
|
|
19
|
+
import { NotificationsModule } from '../modules/notifications';
|
|
20
|
+
import { OAuthModule } from '../modules/oauth';
|
|
21
|
+
import { PhotosModule } from '../modules/photos';
|
|
22
|
+
import { PushModule } from '../modules/push';
|
|
23
|
+
import { ReviewsModule } from '../modules/reviews';
|
|
24
|
+
import { ScreenModule } from '../modules/screen';
|
|
25
|
+
import { ShareModule } from '../modules/share';
|
|
26
|
+
import { StorageModule } from '../modules/storage';
|
|
27
|
+
import { ToastModule } from '../modules/toast';
|
|
28
|
+
import { UiModule } from '../modules/ui';
|
|
29
|
+
|
|
30
|
+
export class NativeKitOptions {
|
|
31
|
+
/** Priority order; first adapter whose detect() passes wins. */
|
|
32
|
+
adapters: NativeKitAdapter[] = [new AppwrapAdapter(), new WebAdapter()];
|
|
33
|
+
handshakeTimeoutMs = 3000;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export class NativeKit {
|
|
37
|
+
public readonly haptics = new HapticsModule(this);
|
|
38
|
+
public readonly share = new ShareModule(this);
|
|
39
|
+
public readonly screen = new ScreenModule(this);
|
|
40
|
+
public readonly storage = new StorageModule(this);
|
|
41
|
+
public readonly toast = new ToastModule(this);
|
|
42
|
+
public readonly ui = new UiModule(this);
|
|
43
|
+
public readonly device = new DeviceModule(this);
|
|
44
|
+
public readonly clipboard = new ClipboardModule(this);
|
|
45
|
+
public readonly notifications = new NotificationsModule(this);
|
|
46
|
+
public readonly push = new PushModule(this);
|
|
47
|
+
public readonly biometrics = new BiometricsModule(this);
|
|
48
|
+
public readonly geo = new GeoModule(this);
|
|
49
|
+
public readonly photos = new PhotosModule(this);
|
|
50
|
+
public readonly network = new NetworkModule(this);
|
|
51
|
+
public readonly lifecycle = new LifecycleModule(this);
|
|
52
|
+
public readonly reviews = new ReviewsModule(this);
|
|
53
|
+
public readonly motion = new MotionModule(this);
|
|
54
|
+
public readonly health = new HealthModule(this);
|
|
55
|
+
public readonly media = new MediaModule(this);
|
|
56
|
+
public readonly contacts = new ContactsModule(this);
|
|
57
|
+
public readonly calendar = new CalendarModule(this);
|
|
58
|
+
public readonly app = new AppModule(this);
|
|
59
|
+
public readonly browser = new BrowserModule(this);
|
|
60
|
+
public readonly oauth = new OAuthModule(this);
|
|
61
|
+
public readonly billing = new BillingModule(this);
|
|
62
|
+
|
|
63
|
+
public handshakeInfo: Handshake | null = null;
|
|
64
|
+
public options: NativeKitOptions;
|
|
65
|
+
private adapter: NativeKitAdapter | null = null;
|
|
66
|
+
private readyPromise: Promise<Handshake> | null = null;
|
|
67
|
+
|
|
68
|
+
constructor(options?: Partial<NativeKitOptions>) {
|
|
69
|
+
this.options = { ...new NativeKitOptions(), ...options };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Resolve the environment and perform the handshake. Idempotent. */
|
|
73
|
+
ready(): Promise<Handshake> {
|
|
74
|
+
if (!this.readyPromise) {
|
|
75
|
+
this.readyPromise = (async () => {
|
|
76
|
+
const adapter = this.options.adapters.find((a) => a.detect());
|
|
77
|
+
if (!adapter) throw new KitError('NOT_READY', 'No adapter detected this environment');
|
|
78
|
+
this.adapter = adapter;
|
|
79
|
+
const handshake = await adapter.handshake(this.options.handshakeTimeoutMs);
|
|
80
|
+
// Version-skew safety net: a native shell from an older `appwrap init` may speak a
|
|
81
|
+
// different protocol. Fail loud rather than silently mis-degrade. The web adapter
|
|
82
|
+
// always reports the kit's own protocol, so this only ever fires against a stale shell.
|
|
83
|
+
if (handshake.protocol !== KIT_PROTOCOL) {
|
|
84
|
+
throw new KitError(
|
|
85
|
+
'UNSUPPORTED',
|
|
86
|
+
`Shell protocol v${handshake.protocol} ≠ kit protocol v${KIT_PROTOCOL} — ` +
|
|
87
|
+
`the native wrapper is out of date. Re-run \`appwrap init\` to regenerate it.`
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
this.handshakeInfo = handshake;
|
|
91
|
+
return this.handshakeInfo;
|
|
92
|
+
})();
|
|
93
|
+
}
|
|
94
|
+
return this.readyPromise;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
get is() {
|
|
98
|
+
return { native: this.adapter?.kind === 'appwrap', web: this.adapter?.kind === 'web' };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
get platform() {
|
|
102
|
+
return this.handshakeInfo?.platform ?? 'web';
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
capability(name: string): Capability {
|
|
106
|
+
return this.handshakeInfo?.capabilities[name] ?? 'none';
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async invoke<T = unknown>(method: string, params?: unknown, opts?: InvokeOptions): Promise<T> {
|
|
110
|
+
if (!this.adapter) await this.ready();
|
|
111
|
+
return this.adapter!.invoke<T>(method, params, opts);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
on(event: string, cb: (payload: unknown) => void): Unsubscribe {
|
|
115
|
+
if (!this.adapter) {
|
|
116
|
+
// Defer subscription until ready() resolves the adapter.
|
|
117
|
+
let unsub: Unsubscribe | null = null;
|
|
118
|
+
let cancelled = false;
|
|
119
|
+
this.ready()
|
|
120
|
+
.then(() => {
|
|
121
|
+
if (!cancelled) unsub = this.adapter!.on(event, cb);
|
|
122
|
+
})
|
|
123
|
+
.catch(() => {}); // ready() failure already surfaces to the ready() caller
|
|
124
|
+
return () => { cancelled = true; unsub?.(); };
|
|
125
|
+
}
|
|
126
|
+
return this.adapter.on(event, cb);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Shared default instance — `import { kit } from '@livx.cc/native-kit'`. */
|
|
131
|
+
export const kit = new NativeKit();
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Envelope,
|
|
3
|
+
Handshake,
|
|
4
|
+
InvokeOptions,
|
|
5
|
+
KitError,
|
|
6
|
+
NativeKitAdapter,
|
|
7
|
+
RequestEnvelope,
|
|
8
|
+
ResponseEnvelope,
|
|
9
|
+
Unsubscribe,
|
|
10
|
+
} from './types';
|
|
11
|
+
|
|
12
|
+
declare global {
|
|
13
|
+
interface Window {
|
|
14
|
+
webkit?: { messageHandlers?: { appwrap?: { postMessage(msg: unknown): void } } };
|
|
15
|
+
appwrapNative?: { postMessage(json: string): void };
|
|
16
|
+
__appwrapDeliver?: (json: string) => void;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Talks appwrap protocol v1 to the native shell. Envelopes travel as JSON strings
|
|
22
|
+
* in both directions (NSDictionary marshalling through WKScriptMessage is lossy in
|
|
23
|
+
* some bridges; strings are not).
|
|
24
|
+
* - web → native: iOS `webkit.messageHandlers.appwrap.postMessage(json)`,
|
|
25
|
+
* Android `appwrapNative.postMessage(json)` (shell-injected shim or JS interface).
|
|
26
|
+
* - native → web: shell evaluates `window.__appwrapDeliver(json)`.
|
|
27
|
+
*/
|
|
28
|
+
export class AppwrapAdapter implements NativeKitAdapter {
|
|
29
|
+
readonly kind = 'appwrap' as const;
|
|
30
|
+
private pending = new Map<string, { resolve: (v: unknown) => void; reject: (e: Error) => void }>();
|
|
31
|
+
private listeners = new Map<string, Set<(payload: unknown) => void>>();
|
|
32
|
+
private seq = 0;
|
|
33
|
+
|
|
34
|
+
detect(): boolean {
|
|
35
|
+
return typeof window !== 'undefined' &&
|
|
36
|
+
(!!window.webkit?.messageHandlers?.appwrap || !!window.appwrapNative);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async handshake(timeoutMs: number): Promise<Handshake> {
|
|
40
|
+
this.installDeliver();
|
|
41
|
+
return this.request<Handshake>('app.handshake', undefined, timeoutMs);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
invoke<T>(method: string, params?: unknown, opts?: InvokeOptions): Promise<T> {
|
|
45
|
+
return this.request<T>(method, params, opts?.timeoutMs ?? 10_000);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
on(event: string, cb: (payload: unknown) => void): Unsubscribe {
|
|
49
|
+
let set = this.listeners.get(event);
|
|
50
|
+
if (!set) this.listeners.set(event, (set = new Set()));
|
|
51
|
+
set.add(cb);
|
|
52
|
+
return () => set!.delete(cb);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private request<T>(method: string, params: unknown, timeoutMs: number): Promise<T> {
|
|
56
|
+
const id = `k${++this.seq}`;
|
|
57
|
+
const envelope: RequestEnvelope = { v: 1, id, kind: 'request', method, params };
|
|
58
|
+
return new Promise<T>((resolve, reject) => {
|
|
59
|
+
const timer = setTimeout(() => {
|
|
60
|
+
this.pending.delete(id);
|
|
61
|
+
reject(new KitError('TIMEOUT', `${method} timed out after ${timeoutMs}ms`));
|
|
62
|
+
}, timeoutMs);
|
|
63
|
+
this.pending.set(id, {
|
|
64
|
+
resolve: (v) => { clearTimeout(timer); resolve(v as T); },
|
|
65
|
+
reject: (e) => { clearTimeout(timer); reject(e); },
|
|
66
|
+
});
|
|
67
|
+
this.post(envelope);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
private post(envelope: RequestEnvelope): void {
|
|
72
|
+
const json = JSON.stringify(envelope);
|
|
73
|
+
if (window.webkit?.messageHandlers?.appwrap) {
|
|
74
|
+
window.webkit.messageHandlers.appwrap.postMessage(json);
|
|
75
|
+
} else if (window.appwrapNative) {
|
|
76
|
+
window.appwrapNative.postMessage(json);
|
|
77
|
+
} else {
|
|
78
|
+
const p = this.pending.get(envelope.id);
|
|
79
|
+
this.pending.delete(envelope.id);
|
|
80
|
+
p?.reject(new KitError('NOT_READY', 'No native transport present'));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private installDeliver(): void {
|
|
85
|
+
if (window.__appwrapDeliver) return;
|
|
86
|
+
window.__appwrapDeliver = (json: string) => {
|
|
87
|
+
let envelope: Envelope;
|
|
88
|
+
try {
|
|
89
|
+
envelope = JSON.parse(json);
|
|
90
|
+
} catch {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (envelope.kind === 'response') this.handleResponse(envelope);
|
|
94
|
+
else if (envelope.kind === 'event') {
|
|
95
|
+
this.listeners.get(envelope.event)?.forEach((cb) => cb(envelope.payload));
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private handleResponse(envelope: ResponseEnvelope): void {
|
|
101
|
+
const p = this.pending.get(envelope.id);
|
|
102
|
+
if (!p) return;
|
|
103
|
+
this.pending.delete(envelope.id);
|
|
104
|
+
if (envelope.error) p.reject(new KitError(envelope.error.code, envelope.error.message));
|
|
105
|
+
else p.resolve(envelope.result);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
export type Platform = 'ios' | 'android' | 'web';
|
|
2
|
+
export type AdapterKind = 'appwrap' | 'web';
|
|
3
|
+
|
|
4
|
+
/** Bridge protocol version this kit speaks. The native shell reports its own in the handshake;
|
|
5
|
+
* `ready()` asserts they match so a kit running against a stale shell fails loud instead of
|
|
6
|
+
* silently mis-degrading. Bump in lockstep with a breaking change to the wire envelope. */
|
|
7
|
+
export const KIT_PROTOCOL = 1 as const;
|
|
8
|
+
|
|
9
|
+
/** How a capability is fulfilled in the current environment. */
|
|
10
|
+
export type Capability = 'native' | 'web' | 'none';
|
|
11
|
+
|
|
12
|
+
export interface AppInfo {
|
|
13
|
+
id: string;
|
|
14
|
+
name: string;
|
|
15
|
+
version: string;
|
|
16
|
+
build?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface Handshake {
|
|
20
|
+
protocol: 1;
|
|
21
|
+
platform: Platform;
|
|
22
|
+
app: AppInfo;
|
|
23
|
+
capabilities: Record<string, Capability>;
|
|
24
|
+
/** Optional diagnostic payload (breadcrumbs, etc.). */
|
|
25
|
+
debug?: Record<string, unknown>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Wire envelope (appwrap protocol v1). Web adapter never serializes these. */
|
|
29
|
+
export interface RequestEnvelope {
|
|
30
|
+
v: 1;
|
|
31
|
+
id: string;
|
|
32
|
+
kind: 'request';
|
|
33
|
+
method: string;
|
|
34
|
+
params?: unknown;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ResponseEnvelope {
|
|
38
|
+
v: 1;
|
|
39
|
+
id: string;
|
|
40
|
+
kind: 'response';
|
|
41
|
+
result?: unknown;
|
|
42
|
+
error?: { code: KitErrorCode; message: string };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface EventEnvelope {
|
|
46
|
+
v: 1;
|
|
47
|
+
kind: 'event';
|
|
48
|
+
event: string;
|
|
49
|
+
payload?: unknown;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export type Envelope = RequestEnvelope | ResponseEnvelope | EventEnvelope;
|
|
53
|
+
|
|
54
|
+
export type KitErrorCode = 'UNSUPPORTED' | 'DENIED' | 'TIMEOUT' | 'NATIVE_ERROR' | 'NOT_READY';
|
|
55
|
+
|
|
56
|
+
export class KitError extends Error {
|
|
57
|
+
constructor(public code: KitErrorCode, message: string) {
|
|
58
|
+
super(message);
|
|
59
|
+
this.name = 'KitError';
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export type Unsubscribe = () => void;
|
|
64
|
+
|
|
65
|
+
export interface InvokeOptions {
|
|
66
|
+
/** Per-call response deadline. Interactive flows (pickers, dialogs, auth
|
|
67
|
+
* prompts) should pass a generous one — the user may take their time. */
|
|
68
|
+
timeoutMs?: number;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface NativeKitAdapter {
|
|
72
|
+
readonly kind: AdapterKind;
|
|
73
|
+
/** Is this environment present? Must be cheap and side-effect free. */
|
|
74
|
+
detect(): boolean;
|
|
75
|
+
handshake(timeoutMs: number): Promise<Handshake>;
|
|
76
|
+
invoke<T = unknown>(method: string, params?: unknown, opts?: InvokeOptions): Promise<T>;
|
|
77
|
+
on(event: string, cb: (payload: unknown) => void): Unsubscribe;
|
|
78
|
+
}
|