@livx.cc/native-kit 0.35.2 → 0.36.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/src/core/NativeKit.ts +49 -3
- package/src/core/types.ts +1 -1
- package/src/index.ts +1 -0
- package/src/modules/app.ts +37 -0
- package/src/modules/notifications.ts +12 -0
- package/src/modules/widget.ts +63 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@livx.cc/native-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.36.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",
|
package/src/core/NativeKit.ts
CHANGED
|
@@ -34,6 +34,7 @@ import { ToastModule } from '../modules/toast';
|
|
|
34
34
|
import { TrackingModule } from '../modules/tracking';
|
|
35
35
|
import { UiModule } from '../modules/ui';
|
|
36
36
|
import { UpdatesModule } from '../modules/updates';
|
|
37
|
+
import { WidgetModule } from '../modules/widget';
|
|
37
38
|
|
|
38
39
|
export class NativeKitOptions {
|
|
39
40
|
/** Priority order; first adapter whose detect() passes wins. */
|
|
@@ -107,10 +108,17 @@ 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 widget = new WidgetModule(this);
|
|
110
112
|
|
|
111
113
|
public handshakeInfo: Handshake | null = null;
|
|
112
114
|
public options: NativeKitOptions;
|
|
113
115
|
private adapter: NativeKitAdapter | null = null;
|
|
116
|
+
/** Web-platform fallback behind a native shell: methods the shell rejects as UNSUPPORTED
|
|
117
|
+
* are retried against the WebAdapter, so a capability the webview itself can satisfy
|
|
118
|
+
* (geolocation, speech, navigator.share, …) works even when the shell ships no handler
|
|
119
|
+
* for it. Capability keys the shell reports as 'none'/absent surface as 'web' when the
|
|
120
|
+
* fallback can fulfil them. */
|
|
121
|
+
private webFallback: NativeKitAdapter | null = null;
|
|
114
122
|
private readyPromise: Promise<Handshake> | null = null;
|
|
115
123
|
private contextPromise: Promise<KitContext> | null = null;
|
|
116
124
|
|
|
@@ -153,6 +161,29 @@ export class NativeKit {
|
|
|
153
161
|
);
|
|
154
162
|
}
|
|
155
163
|
this.handshakeInfo = handshake;
|
|
164
|
+
// Hybrid degrade: pair a native shell with the web adapter for anything the shell
|
|
165
|
+
// doesn't implement. WebAdapter.handshake() is pure feature detection (no side
|
|
166
|
+
// effects), so probing it here is safe.
|
|
167
|
+
if (this.adapter!.kind !== 'web') {
|
|
168
|
+
const web = this.options.adapters.find((a) => a.kind === 'web');
|
|
169
|
+
if (web?.detect()) {
|
|
170
|
+
try {
|
|
171
|
+
const webHs = await web.handshake(this.options.handshakeTimeoutMs);
|
|
172
|
+
this.webFallback = web;
|
|
173
|
+
for (const [key, value] of Object.entries(webHs.capabilities)) {
|
|
174
|
+
// Only ABSENT keys are upgraded. An explicit 'none' from the shell is a
|
|
175
|
+
// veto — it means "this platform genuinely can't", not "no handler" (e.g.
|
|
176
|
+
// desktop reports motion:'none': DeviceMotionEvent exists in the webview
|
|
177
|
+
// but never fires without an accelerometer, so 'web' would be a lie).
|
|
178
|
+
if (handshake.capabilities[key] === undefined && value !== 'none') {
|
|
179
|
+
handshake.capabilities[key] = 'web';
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
} catch (e) {
|
|
183
|
+
console.warn('[native-kit] web fallback probe failed', e);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
156
187
|
// Zero-config: a native server-loader app begins polling for remote updates.
|
|
157
188
|
this.updates.__autostart();
|
|
158
189
|
// A background launch carries the wake id in the handshake → dispatch the registered handler
|
|
@@ -229,7 +260,14 @@ export class NativeKit {
|
|
|
229
260
|
|
|
230
261
|
async invoke<T = unknown>(method: string, params?: unknown, opts?: InvokeOptions): Promise<T> {
|
|
231
262
|
if (!this.adapter) await this.ready();
|
|
232
|
-
|
|
263
|
+
try {
|
|
264
|
+
return await this.adapter!.invoke<T>(method, params, opts);
|
|
265
|
+
} catch (e) {
|
|
266
|
+
if (this.webFallback && e instanceof KitError && e.code === 'UNSUPPORTED') {
|
|
267
|
+
return this.webFallback.invoke<T>(method, params, opts);
|
|
268
|
+
}
|
|
269
|
+
throw e;
|
|
270
|
+
}
|
|
233
271
|
}
|
|
234
272
|
|
|
235
273
|
on(event: string, cb: (payload: unknown) => void): Unsubscribe {
|
|
@@ -239,12 +277,20 @@ export class NativeKit {
|
|
|
239
277
|
let cancelled = false;
|
|
240
278
|
this.ready()
|
|
241
279
|
.then(() => {
|
|
242
|
-
if (!cancelled) unsub = this.
|
|
280
|
+
if (!cancelled) unsub = this.subscribe(event, cb);
|
|
243
281
|
})
|
|
244
282
|
.catch(() => {}); // ready() failure already surfaces to the ready() caller
|
|
245
283
|
return () => { cancelled = true; unsub?.(); };
|
|
246
284
|
}
|
|
247
|
-
return this.
|
|
285
|
+
return this.subscribe(event, cb);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** Listen on the active adapter AND the web fallback — a watch that fell back to the
|
|
289
|
+
* WebAdapter (geo.position, motion.data) emits its events there, not on the shell. */
|
|
290
|
+
private subscribe(event: string, cb: (payload: unknown) => void): Unsubscribe {
|
|
291
|
+
const unsubs = [this.adapter!.on(event, cb)];
|
|
292
|
+
if (this.webFallback) unsubs.push(this.webFallback.on(event, cb));
|
|
293
|
+
return () => unsubs.forEach((u) => u());
|
|
248
294
|
}
|
|
249
295
|
}
|
|
250
296
|
|
package/src/core/types.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -42,6 +42,7 @@ export type { CalendarEventOptions } from './modules/calendar';
|
|
|
42
42
|
export type { BrowserOptions } from './modules/browser';
|
|
43
43
|
export type { OAuthAuthorizeParams, OAuthResult } from './modules/oauth';
|
|
44
44
|
export type { TrackingStatus } from './modules/tracking';
|
|
45
|
+
export type { WidgetEntry, WidgetPayload, WidgetStat, WidgetPublishResult } from './modules/widget';
|
|
45
46
|
export type {
|
|
46
47
|
AppleSignInName,
|
|
47
48
|
AppleSignInParams,
|
package/src/modules/app.ts
CHANGED
|
@@ -9,6 +9,26 @@ export interface AppShortcut {
|
|
|
9
9
|
subtitle?: string;
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
/** A standalone home-screen icon to pin via {@link AppModule.pinShortcut}. Unlike {@link AppShortcut}
|
|
13
|
+
* it carries its own `url` (the deep link the icon opens) and an optional `iconUrl` tile image. */
|
|
14
|
+
export interface PinShortcut {
|
|
15
|
+
/** Stable id — dedups repeat pins of the same target. */
|
|
16
|
+
id: string;
|
|
17
|
+
title: string;
|
|
18
|
+
/** Full deep link the pinned icon opens (e.g. `myapp://a/123`). */
|
|
19
|
+
url: string;
|
|
20
|
+
/** Tile image URL; fetched to a bitmap. Falls back to the app launcher icon on failure/omission. */
|
|
21
|
+
iconUrl?: string;
|
|
22
|
+
subtitle?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface PinShortcutResult {
|
|
26
|
+
/** True once the OS accepted the pin request (the user still confirms the system prompt). */
|
|
27
|
+
pinned: boolean;
|
|
28
|
+
/** Present when not pinned: `'unsupported'` (iOS/web/old Android/incapable launcher). */
|
|
29
|
+
reason?: 'unsupported';
|
|
30
|
+
}
|
|
31
|
+
|
|
12
32
|
/** Where this build was installed from. Drives beta-vs-prod analytics cohorts.
|
|
13
33
|
* - `appstore` / `playstore` — public store install
|
|
14
34
|
* - `testflight` — iOS beta
|
|
@@ -111,4 +131,21 @@ export class AppModule {
|
|
|
111
131
|
onShortcut(cb: (id: string) => void): Unsubscribe {
|
|
112
132
|
return this.kit.on('app.shortcut', (p) => cb((p as { id: string }).id));
|
|
113
133
|
}
|
|
134
|
+
|
|
135
|
+
/** 'native' where the OS can pin a STANDALONE launcher icon on request (Android 8+); iOS has no such
|
|
136
|
+
* API (`{ios:false}`), so on iOS this reads 'none' — surface a widget there instead. Branch on this. */
|
|
137
|
+
get pinShortcutCapability() {
|
|
138
|
+
return this.kit.capability('pinShortcut');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Ask the OS to add a standalone home-screen icon for a deep target (distinct from {@link setShortcuts},
|
|
143
|
+
* which is the app-icon long-press menu). Android 8+ shows the system add-to-home prompt; unsupported
|
|
144
|
+
* launchers / iOS / web resolve `{pinned:false}`. `url` is the full deep link the icon opens (e.g.
|
|
145
|
+
* `myapp://a/123`) — routed through the app's normal deep-link handling. `iconUrl` is fetched to a tile
|
|
146
|
+
* (falls back to the launcher icon). Returns whether the pin request was accepted by the system.
|
|
147
|
+
*/
|
|
148
|
+
pinShortcut(shortcut: PinShortcut): Promise<PinShortcutResult> {
|
|
149
|
+
return this.kit.invoke('app.pinShortcut', shortcut);
|
|
150
|
+
}
|
|
114
151
|
}
|
|
@@ -12,6 +12,18 @@ export interface ScheduleOptions {
|
|
|
12
12
|
* `kit.lifecycle.onDeepLink` routes it with no extra wiring.
|
|
13
13
|
*/
|
|
14
14
|
deepLink?: string;
|
|
15
|
+
/**
|
|
16
|
+
* Display name of the notification's SENDER (e.g. a mini-app's name). When set,
|
|
17
|
+
* iOS renders a communication-style notification (iOS 15+) and Android posts on a
|
|
18
|
+
* per-sender channel, both showing this name as the "from" identity — the original
|
|
19
|
+
* `title` is demoted to the subtitle. Falls back to a plain notification otherwise.
|
|
20
|
+
*/
|
|
21
|
+
sender?: string;
|
|
22
|
+
/**
|
|
23
|
+
* Sender avatar — an image URL or data-URI. Rendered as the circular avatar on
|
|
24
|
+
* iOS (INImage) and the large icon on Android. Ignored if it can't be loaded.
|
|
25
|
+
*/
|
|
26
|
+
icon?: string;
|
|
15
27
|
}
|
|
16
28
|
|
|
17
29
|
export class NotificationsModule {
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { NativeKit } from '../core/NativeKit';
|
|
2
|
+
|
|
3
|
+
/** One tile/row in an icon-grid or list widget. `deepLink` is the full URL the tile opens (routed by
|
|
4
|
+
* the app's deep-link handling); `iconUrl` is fetched to a tile image on the device. */
|
|
5
|
+
export interface WidgetEntry {
|
|
6
|
+
label: string;
|
|
7
|
+
sublabel?: string;
|
|
8
|
+
deepLink?: string;
|
|
9
|
+
iconUrl?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** A single big number for the `stat` template. */
|
|
13
|
+
export interface WidgetStat {
|
|
14
|
+
value: string;
|
|
15
|
+
label?: string;
|
|
16
|
+
sublabel?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Payload published to the home-screen widget. `template` picks the layout:
|
|
20
|
+
* - `icon-grid` — grid of {@link WidgetEntry} tiles (the app-launcher use)
|
|
21
|
+
* - `list` — rows of {@link WidgetEntry}
|
|
22
|
+
* - `stat` — one {@link WidgetStat} (optional whole-widget `deepLink`) */
|
|
23
|
+
export interface WidgetPayload {
|
|
24
|
+
template: 'icon-grid' | 'list' | 'stat';
|
|
25
|
+
title?: string;
|
|
26
|
+
entries?: WidgetEntry[];
|
|
27
|
+
stat?: WidgetStat;
|
|
28
|
+
/** Whole-widget deep link (stat template, or the empty-state fallback). */
|
|
29
|
+
deepLink?: string;
|
|
30
|
+
/** Which home-screen widget kind this targets. `launcher` (default) = the app-launcher/icon-grid
|
|
31
|
+
* widget; `data` = the separate data widget an in-app view can drive. Two kinds so a data publish
|
|
32
|
+
* never clobbers the launcher (each renders from its own shared-store key). */
|
|
33
|
+
slot?: 'launcher' | 'data';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface WidgetPublishResult {
|
|
37
|
+
published: boolean;
|
|
38
|
+
reason?: 'unsupported';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Publish content to the app's home-screen widget (iOS WidgetKit / Android AppWidget). The shell writes
|
|
43
|
+
* the payload into a shared container the widget reads at render, fetching any `iconUrl` to a local
|
|
44
|
+
* tile image, then reloads the widget. Web/unsupported builds resolve `{published:false}`. Branch on
|
|
45
|
+
* {@link WidgetModule.capability}.
|
|
46
|
+
*/
|
|
47
|
+
export class WidgetModule {
|
|
48
|
+
constructor(private kit: NativeKit) {}
|
|
49
|
+
|
|
50
|
+
/** 'native' where a home-screen widget surface exists (iOS 14+ / Android AppWidget) · else 'none'. */
|
|
51
|
+
get capability() {
|
|
52
|
+
return this.kit.capability('widget');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
publish(payload: WidgetPayload): Promise<WidgetPublishResult> {
|
|
56
|
+
return this.kit.invoke('widget.publish', payload);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Clear the widget (renders its empty state). */
|
|
60
|
+
clear(): Promise<WidgetPublishResult> {
|
|
61
|
+
return this.kit.invoke('widget.publish', { template: 'icon-grid', entries: [] });
|
|
62
|
+
}
|
|
63
|
+
}
|