@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
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { NativeKit } from '../core/NativeKit';
|
|
2
|
+
import type { Unsubscribe } from '../core/types';
|
|
3
|
+
|
|
4
|
+
export interface NetworkStatus {
|
|
5
|
+
online: boolean;
|
|
6
|
+
/** 'wifi' | 'cellular' | 'none' | 'unknown' */
|
|
7
|
+
type: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class NetworkModule {
|
|
11
|
+
constructor(private kit: NativeKit) {}
|
|
12
|
+
|
|
13
|
+
get capability() {
|
|
14
|
+
return this.kit.capability('network');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
status(): Promise<NetworkStatus> {
|
|
18
|
+
return this.kit.invoke('network.status');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
onChange(cb: (status: NetworkStatus) => void): Unsubscribe {
|
|
22
|
+
return this.kit.on('network.change', (p) => cb(p as NetworkStatus));
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { NativeKit } from '../core/NativeKit';
|
|
2
|
+
|
|
3
|
+
export interface ScheduleOptions {
|
|
4
|
+
id?: number;
|
|
5
|
+
title: string;
|
|
6
|
+
body?: string;
|
|
7
|
+
/** Seconds from now (default 1). */
|
|
8
|
+
delaySec?: number;
|
|
9
|
+
/**
|
|
10
|
+
* Deep-link URL fired when the notification is tapped (e.g. 'app://item/7').
|
|
11
|
+
* Delivered through the same `deeplink.open` event as an external open, so
|
|
12
|
+
* `kit.lifecycle.onDeepLink` routes it with no extra wiring.
|
|
13
|
+
*/
|
|
14
|
+
deepLink?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class NotificationsModule {
|
|
18
|
+
constructor(private kit: NativeKit) {}
|
|
19
|
+
|
|
20
|
+
get capability() {
|
|
21
|
+
return this.kit.capability('notifications');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** 'granted' | 'denied' | 'default' */
|
|
25
|
+
requestPermission(): Promise<string> {
|
|
26
|
+
return this.kit.invoke('notifications.requestPermission', undefined, { timeoutMs: 60_000 });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
schedule(options: ScheduleOptions): Promise<{ id: number }> {
|
|
30
|
+
return this.kit.invoke('notifications.schedule', options);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Count of pending (scheduled, not yet delivered) notifications. */
|
|
34
|
+
pending(): Promise<number> {
|
|
35
|
+
return this.kit.invoke('notifications.pending');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
setBadge(count: number): Promise<void> {
|
|
39
|
+
return this.kit.invoke('notifications.setBadge', { count });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
clear(): Promise<void> {
|
|
43
|
+
return this.kit.invoke('notifications.clear');
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { NativeKit } from '../core/NativeKit';
|
|
2
|
+
|
|
3
|
+
export interface OAuthAuthorizeParams {
|
|
4
|
+
/** Full authorization URL — provider endpoint + query (client_id, redirect_uri, scope, PKCE…). */
|
|
5
|
+
url: string;
|
|
6
|
+
/** The custom URL scheme the redirect_uri uses (e.g. 'circles' for circles://oauth). The system
|
|
7
|
+
* browser sheet closes + returns as soon as it sees a redirect to this scheme. */
|
|
8
|
+
callbackScheme: string;
|
|
9
|
+
/** Don't persist browser cookies (fresh provider login every time). Default false — reuses the
|
|
10
|
+
* existing provider session so the user isn't re-typing their Google password. */
|
|
11
|
+
ephemeral?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface OAuthResult {
|
|
15
|
+
/** The full callback URL the provider redirected to, including ?code=…&state=… (or #fragment). */
|
|
16
|
+
url: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* System-browser OAuth via ASWebAuthenticationSession (iOS). Provider-agnostic by design — the same
|
|
21
|
+
* philosophy as `kit.push`: the shell owns only the device-side primitive (run the auth handshake in
|
|
22
|
+
* the OS-shared, policy-compliant browser), while the PWA builds the auth URL and does token exchange.
|
|
23
|
+
*
|
|
24
|
+
* WHY: Google (and others) reject OAuth loaded inside an embedded WebView with
|
|
25
|
+
* `403 disallowed_useragent`. ASWebAuthenticationSession is the compliant browser, so the flow
|
|
26
|
+
* succeeds and returns the redirect to your custom scheme.
|
|
27
|
+
*
|
|
28
|
+
* Web fallback: capability is 'none' — the PWA should use its normal web OAuth (popup/redirect) when
|
|
29
|
+
* `kit.oauth.capability !== 'native'`.
|
|
30
|
+
*/
|
|
31
|
+
export class OAuthModule {
|
|
32
|
+
constructor(private kit: NativeKit) {}
|
|
33
|
+
|
|
34
|
+
get capability() {
|
|
35
|
+
return this.kit.capability('oauth');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
authorize(params: OAuthAuthorizeParams): Promise<OAuthResult> {
|
|
39
|
+
return this.kit.invoke('oauth.authorize', params as unknown as Record<string, unknown>);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { NativeKit } from '../core/NativeKit';
|
|
2
|
+
|
|
3
|
+
export interface PickPhotoOptions {
|
|
4
|
+
/** Return the image as a downscaled JPEG data URL the PWA can render/upload. */
|
|
5
|
+
dataUrl?: boolean;
|
|
6
|
+
/** Longest-edge cap in px for the returned dataUrl (default 1024). */
|
|
7
|
+
maxSize?: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface PickedPhoto {
|
|
11
|
+
picked: boolean;
|
|
12
|
+
/** Present when picked: dimensions of the selected image. */
|
|
13
|
+
width?: number;
|
|
14
|
+
height?: number;
|
|
15
|
+
/** Present when `dataUrl` was requested: `data:image/jpeg;base64,...`. */
|
|
16
|
+
dataUrl?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class PhotosModule {
|
|
20
|
+
constructor(private kit: NativeKit) {}
|
|
21
|
+
|
|
22
|
+
get capability() {
|
|
23
|
+
return this.kit.capability('photos');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
get cameraCapability() {
|
|
27
|
+
return this.kit.capability('camera');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Open the system photo picker; resolves { picked: false } when dismissed. */
|
|
31
|
+
pick(options?: PickPhotoOptions): Promise<PickedPhoto> {
|
|
32
|
+
return this.kit.invoke('photos.pick', options, { timeoutMs: 120_000 });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Capture a photo with the camera. UNSUPPORTED where no camera exists (e.g. simulator). */
|
|
36
|
+
capture(options?: PickPhotoOptions): Promise<PickedPhoto> {
|
|
37
|
+
return this.kit.invoke('camera.capture', options, { timeoutMs: 120_000 });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { NativeKit } from '../core/NativeKit';
|
|
2
|
+
import { KitError, type Capability, type Unsubscribe } from '../core/types';
|
|
3
|
+
|
|
4
|
+
/** Which transport produced the token — the consumer's backend needs this to pick the right sender. */
|
|
5
|
+
export type PushPlatform = 'apns' | 'fcm';
|
|
6
|
+
|
|
7
|
+
/** A device push token + its transport. The kit's job ENDS here: ship this to YOUR backend
|
|
8
|
+
* (Bodify / FCM-direct / OneSignal / custom) — the framework never sends, so it's push-platform-agnostic. */
|
|
9
|
+
export interface PushToken {
|
|
10
|
+
platform: PushPlatform;
|
|
11
|
+
token: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** A remote message surfaced to the page. `data` is the sender's custom payload; `title`/`body`
|
|
15
|
+
* are present when the sender included a display (aps.alert / FCM notification). */
|
|
16
|
+
export interface PushMessage {
|
|
17
|
+
data: Record<string, unknown>;
|
|
18
|
+
title?: string;
|
|
19
|
+
body?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Remote push — ONE API across iOS (APNs) and Android (FCM). DEVICE SIDE ONLY:
|
|
24
|
+
* the kit acquires the token + surfaces incoming messages/taps; SENDING is your
|
|
25
|
+
* backend's job (see {@link PushToken}). Provider-agnostic by construction.
|
|
26
|
+
*
|
|
27
|
+
* ```ts
|
|
28
|
+
* if (kit.push.capability === 'native') {
|
|
29
|
+
* if (await kit.push.requestPermission() === 'granted') {
|
|
30
|
+
* const { platform, token } = await kit.push.register();
|
|
31
|
+
* await fetch('/api/push/register', { method:'POST', body: JSON.stringify({ platform, token }) });
|
|
32
|
+
* }
|
|
33
|
+
* kit.push.onMessage((m) => …); // foreground delivery
|
|
34
|
+
* kit.push.onTap((m) => …); // user opened a notification
|
|
35
|
+
* }
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export class PushModule {
|
|
39
|
+
constructor(private kit: NativeKit) {}
|
|
40
|
+
|
|
41
|
+
/** 'native' on a push-enabled shell (APNs/FCM configured), else 'none'. Web push (VAPID)
|
|
42
|
+
* is the app's own concern — the kit reports 'none' rather than pretend to own it. */
|
|
43
|
+
get capability(): Capability {
|
|
44
|
+
return this.kit.is.native ? this.kit.capability('push') : 'none';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Prompt for notification authorization. 'granted' | 'denied'. */
|
|
48
|
+
requestPermission(): Promise<'granted' | 'denied'> {
|
|
49
|
+
return this.kit.invoke('push.requestPermission', undefined, { timeoutMs: 60_000 });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Register with APNs/FCM and resolve the device token. Call after permission is granted. */
|
|
53
|
+
register(): Promise<PushToken> {
|
|
54
|
+
return this.kit.invoke('push.register', undefined, { timeoutMs: 30_000 });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Stop receiving remote pushes (iOS unregisterForRemoteNotifications / FCM deleteToken). */
|
|
58
|
+
unregister(): Promise<void> {
|
|
59
|
+
return this.kit.invoke('push.unregister');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Foreground message delivery (app open, no tap). */
|
|
63
|
+
onMessage(cb: (m: PushMessage) => void): Unsubscribe {
|
|
64
|
+
return this.kit.on('push.message', (p) => cb(p as PushMessage));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** User tapped a notification — carries the same payload + drives navigation. */
|
|
68
|
+
onTap(cb: (m: PushMessage) => void): Unsubscribe {
|
|
69
|
+
return this.kit.on('push.tap', (p) => cb(p as PushMessage));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { NativeKit } from '../core/NativeKit';
|
|
2
|
+
|
|
3
|
+
/** Store review prompt — iOS SKStoreReviewController. The OS rate-limits display. */
|
|
4
|
+
export class ReviewsModule {
|
|
5
|
+
constructor(private kit: NativeKit) {}
|
|
6
|
+
|
|
7
|
+
get capability() {
|
|
8
|
+
return this.kit.capability('reviews');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Ask the OS to show the in-app review prompt. Fire-and-forget; the OS decides. */
|
|
12
|
+
requestReview(): Promise<void> {
|
|
13
|
+
return this.kit.invoke('reviews.requestReview');
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { NativeKit } from '../core/NativeKit';
|
|
2
|
+
import type { Unsubscribe } from '../core/types';
|
|
3
|
+
|
|
4
|
+
export type Orientation = 'portrait' | 'landscape';
|
|
5
|
+
/** What to lock to. 'any' releases the lock (same as {@link OrientationController.unlock}). */
|
|
6
|
+
export type OrientationLock =
|
|
7
|
+
| 'portrait'
|
|
8
|
+
| 'portrait-upside-down'
|
|
9
|
+
| 'landscape'
|
|
10
|
+
| 'landscape-left'
|
|
11
|
+
| 'landscape-right'
|
|
12
|
+
| 'any';
|
|
13
|
+
|
|
14
|
+
/** Screen-level controls. Today: orientation; brightness/keepAwake live on `kit.ui`. */
|
|
15
|
+
export class ScreenModule {
|
|
16
|
+
readonly orientation: OrientationController;
|
|
17
|
+
|
|
18
|
+
constructor(kit: NativeKit) {
|
|
19
|
+
this.orientation = new OrientationController(kit);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Lock/observe the device orientation — ONE API across platforms.
|
|
25
|
+
*
|
|
26
|
+
* Native pins the app (iOS supported-orientations + geometry update, Android
|
|
27
|
+
* `setRequestedOrientation`). On web the Screen Orientation API needs fullscreen
|
|
28
|
+
* and is widely unsupported (desktop, iOS Safari) — `lock` rejects with
|
|
29
|
+
* `KitError('UNSUPPORTED')` there; gate on {@link capability} or catch it.
|
|
30
|
+
*/
|
|
31
|
+
export class OrientationController {
|
|
32
|
+
constructor(private kit: NativeKit) {}
|
|
33
|
+
|
|
34
|
+
/** 'native' on a shell · 'web' where the Screen Orientation API exists · else 'none'. */
|
|
35
|
+
get capability() {
|
|
36
|
+
return this.kit.capability('orientation');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
lock(orientation: OrientationLock): Promise<void> {
|
|
40
|
+
return this.kit.invoke('screen.orientation.lock', { orientation });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Release the lock (rotate freely again). */
|
|
44
|
+
unlock(): Promise<void> {
|
|
45
|
+
return this.kit.invoke('screen.orientation.unlock');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
current(): Promise<Orientation> {
|
|
49
|
+
return this.kit.invoke('screen.orientation.current');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
onChange(cb: (orientation: Orientation) => void): Unsubscribe {
|
|
53
|
+
return this.kit.on('screen.orientation.change', (p) => cb(p as Orientation));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { NativeKit } from '../core/NativeKit';
|
|
2
|
+
|
|
3
|
+
export interface SharePayload {
|
|
4
|
+
title?: string;
|
|
5
|
+
text?: string;
|
|
6
|
+
url?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** A file to share. `base64` is raw base64 (no `data:` prefix) — JSON-safe over the bridge. */
|
|
10
|
+
export interface ShareFile {
|
|
11
|
+
name: string; // filename incl. extension, e.g. 'invoice.pdf'
|
|
12
|
+
mimeType: string; // e.g. 'application/pdf', 'image/png'
|
|
13
|
+
base64: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class ShareModule {
|
|
17
|
+
constructor(private kit: NativeKit) {}
|
|
18
|
+
|
|
19
|
+
/** Text/url share — iOS share sheet, Android chooser, web `navigator.share`. */
|
|
20
|
+
get capability() {
|
|
21
|
+
return this.kit.capability('share');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** File share — distinct flag (a platform can share text but not files). */
|
|
25
|
+
get filesCapability() {
|
|
26
|
+
return this.kit.capability('shareFiles');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
share(payload: SharePayload): Promise<void> {
|
|
30
|
+
return this.kit.invoke('share.share', payload);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Share one or more files (iOS UIActivity / web `navigator.share({files})`). */
|
|
34
|
+
files(files: ShareFile[], opts?: { title?: string; text?: string }): Promise<void> {
|
|
35
|
+
return this.kit.invoke('share.files', { files, ...opts });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { NativeKit } from '../core/NativeKit';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Secure KV — native: Keychain/Keystore. No web fallback (capability 'none');
|
|
5
|
+
* localStorage is not a secret store.
|
|
6
|
+
*/
|
|
7
|
+
export class SecureStorage {
|
|
8
|
+
constructor(private kit: NativeKit) {}
|
|
9
|
+
|
|
10
|
+
get capability() {
|
|
11
|
+
return this.kit.capability('secureStorage');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
get(key: string): Promise<string | null> {
|
|
15
|
+
return this.kit.invoke('storage.secure.get', { key });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
set(key: string, value: string): Promise<void> {
|
|
19
|
+
return this.kit.invoke('storage.secure.set', { key, value });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
remove(key: string): Promise<void> {
|
|
23
|
+
return this.kit.invoke('storage.secure.remove', { key });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Simple KV storage. Native: ApplicationSettings (survives WebView data clears). Web: localStorage. */
|
|
28
|
+
export class StorageModule {
|
|
29
|
+
public readonly secure: SecureStorage;
|
|
30
|
+
|
|
31
|
+
constructor(private kit: NativeKit) {
|
|
32
|
+
this.secure = new SecureStorage(kit);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
get capability() {
|
|
36
|
+
return this.kit.capability('storage');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
get<T = unknown>(key: string): Promise<T | null> {
|
|
40
|
+
return this.kit.invoke('storage.get', { key });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
set(key: string, value: unknown): Promise<void> {
|
|
44
|
+
return this.kit.invoke('storage.set', { key, value });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
remove(key: string): Promise<void> {
|
|
48
|
+
return this.kit.invoke('storage.remove', { key });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Remove every kit-stored key (does not touch the secure store). */
|
|
52
|
+
clear(): Promise<void> {
|
|
53
|
+
return this.kit.invoke('storage.clear');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { NativeKit } from '../core/NativeKit';
|
|
2
|
+
|
|
3
|
+
export class ToastModule {
|
|
4
|
+
constructor(private kit: NativeKit) {}
|
|
5
|
+
|
|
6
|
+
get capability() {
|
|
7
|
+
return this.kit.capability('toast');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
show(message: string, duration: 'short' | 'long' = 'short'): Promise<void> {
|
|
11
|
+
return this.kit.invoke('toast.show', { message, duration });
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import type { NativeKit } from '../core/NativeKit';
|
|
2
|
+
import type { Unsubscribe } from '../core/types';
|
|
3
|
+
|
|
4
|
+
export interface SafeAreaInsets {
|
|
5
|
+
top: number;
|
|
6
|
+
bottom: number;
|
|
7
|
+
left: number;
|
|
8
|
+
right: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface AlertOptions {
|
|
12
|
+
title?: string;
|
|
13
|
+
message: string;
|
|
14
|
+
ok?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ConfirmOptions {
|
|
18
|
+
title?: string;
|
|
19
|
+
message: string;
|
|
20
|
+
ok?: string;
|
|
21
|
+
cancel?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ActionOptions {
|
|
25
|
+
title?: string;
|
|
26
|
+
options: string[];
|
|
27
|
+
cancel?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Perceived-luminance test for #rgb / #rrggbb — true when the color is light. */
|
|
31
|
+
function isLightColor(hex: string): boolean {
|
|
32
|
+
const m = hex.trim().replace('#', '');
|
|
33
|
+
const full = m.length === 3 ? m.split('').map((c) => c + c).join('') : m;
|
|
34
|
+
if (full.length < 6) return false;
|
|
35
|
+
const r = parseInt(full.slice(0, 2), 16);
|
|
36
|
+
const g = parseInt(full.slice(2, 4), 16);
|
|
37
|
+
const b = parseInt(full.slice(4, 6), 16);
|
|
38
|
+
// Rec. 601 luma; >0.6 reads as a light surface needing dark icons.
|
|
39
|
+
return (0.299 * r + 0.587 * g + 0.114 * b) / 255 > 0.6;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export class UiModule {
|
|
43
|
+
constructor(private kit: NativeKit) {}
|
|
44
|
+
|
|
45
|
+
get statusBarCapability() {
|
|
46
|
+
return this.kit.capability('statusBar');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
get screenCapability() {
|
|
50
|
+
return this.kit.capability('screen');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
get dialogsCapability() {
|
|
54
|
+
return this.kit.capability('dialogs');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
get themeColorCapability() {
|
|
58
|
+
return this.kit.capability('themeColor');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** 'light' = white icons (dark backgrounds), 'dark' = black icons. */
|
|
62
|
+
setStatusBarStyle(style: 'light' | 'dark'): Promise<void> {
|
|
63
|
+
return this.kit.invoke('ui.statusBar.setStyle', { style });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
safeArea(): Promise<SafeAreaInsets> {
|
|
67
|
+
return this.kit.invoke('ui.safeArea');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** 0..1 */
|
|
71
|
+
getBrightness(): Promise<number> {
|
|
72
|
+
return this.kit.invoke('ui.brightness.get');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
setBrightness(level: number): Promise<void> {
|
|
76
|
+
return this.kit.invoke('ui.brightness.set', { level });
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Prevent the screen from sleeping. */
|
|
80
|
+
keepAwake(on: boolean): Promise<void> {
|
|
81
|
+
return this.kit.invoke('ui.keepAwake', { on });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ── dialogs ──────────────────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
alert(opts: AlertOptions): Promise<void> {
|
|
87
|
+
return this.kit.invoke('ui.alert', opts, { timeoutMs: 120_000 });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Resolves true on OK, false on cancel. */
|
|
91
|
+
confirm(opts: ConfirmOptions): Promise<boolean> {
|
|
92
|
+
return this.kit.invoke('ui.confirm', opts, { timeoutMs: 120_000 });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Action sheet — resolves the chosen option's index, or null on cancel. */
|
|
96
|
+
action(opts: ActionOptions): Promise<number | null> {
|
|
97
|
+
return this.kit.invoke('ui.action', opts, { timeoutMs: 120_000 });
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ── theme color ──────────────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
/** Tint the native chrome behind the page (status bar / safe areas). */
|
|
103
|
+
setBackgroundColor(color: string): Promise<void> {
|
|
104
|
+
return this.kit.invoke('ui.setBackgroundColor', { color });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Keep the native chrome in sync with `<meta name="theme-color">`.
|
|
109
|
+
* Applies the current value immediately and observes changes: tints the
|
|
110
|
+
* native root (visible during launch / rotation / keyboard gaps) AND flips
|
|
111
|
+
* the status-bar icon style to keep contrast against the theme color.
|
|
112
|
+
* No-op on web — the browser honors the meta tag natively.
|
|
113
|
+
*/
|
|
114
|
+
syncThemeColor(): Unsubscribe {
|
|
115
|
+
if (this.themeColorCapability !== 'native') return () => {};
|
|
116
|
+
const apply = () => {
|
|
117
|
+
const meta = document.querySelector<HTMLMetaElement>('meta[name="theme-color"]');
|
|
118
|
+
if (!meta?.content) return;
|
|
119
|
+
this.setBackgroundColor(meta.content).catch((e) =>
|
|
120
|
+
console.warn('[native-kit] setBackgroundColor failed', e)
|
|
121
|
+
);
|
|
122
|
+
// Light theme color → dark icons; dark theme color → light icons.
|
|
123
|
+
this.setStatusBarStyle(isLightColor(meta.content) ? 'dark' : 'light').catch(() => {});
|
|
124
|
+
};
|
|
125
|
+
apply();
|
|
126
|
+
const observer = new MutationObserver(apply);
|
|
127
|
+
observer.observe(document.head, {
|
|
128
|
+
subtree: true,
|
|
129
|
+
childList: true,
|
|
130
|
+
attributes: true,
|
|
131
|
+
attributeFilter: ['content'],
|
|
132
|
+
});
|
|
133
|
+
return () => observer.disconnect();
|
|
134
|
+
}
|
|
135
|
+
}
|