@livx.cc/native-kit 0.31.0 → 0.31.1

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.31.0",
3
+ "version": "0.31.1",
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",
@@ -1,3 +1,4 @@
1
+ /// <reference path="../web-experimental.d.ts" />
1
2
  import { Capability, Handshake, KitError, NativeKitAdapter, Unsubscribe } from './types';
2
3
 
3
4
  /** Decode raw base64 → an ArrayBuffer for File construction (no `data:` prefix expected). */
@@ -17,7 +18,7 @@ function bufferToBase64(buf: ArrayBuffer): string {
17
18
  }
18
19
 
19
20
  /** Map our lock vocabulary to valid ScreenOrientation `OrientationLockType` values. */
20
- function toWebOrientation(o: string): any {
21
+ function toWebOrientation(o: string): string {
21
22
  switch (o) {
22
23
  case 'portrait-upside-down': return 'portrait-secondary';
23
24
  case 'landscape-left': return 'landscape-primary';
@@ -26,6 +27,20 @@ function toWebOrientation(o: string): any {
26
27
  }
27
28
  }
28
29
 
30
+ /** Options accepted by `speech.speak` (a subset of the wire params, all optional). */
31
+ interface SpeakOptions {
32
+ lang?: string;
33
+ rate?: number;
34
+ pitch?: number;
35
+ voice?: string;
36
+ }
37
+
38
+ /** Options accepted by `speech.listen`. */
39
+ interface ListenOptions {
40
+ lang?: string;
41
+ partial?: boolean;
42
+ }
43
+
29
44
  /**
30
45
  * Browser fallback adapter. Fulfills what the Web Platform can, reports honest
31
46
  * capability flags for the rest. Methods for unsupported capabilities resolve
@@ -34,7 +49,7 @@ function toWebOrientation(o: string): any {
34
49
  export class WebAdapter implements NativeKitAdapter {
35
50
  readonly kind = 'web' as const;
36
51
  private toastEl: HTMLElement | null = null;
37
- private wakeLock: any = null;
52
+ private wakeLock: WakeLockSentinel | null = null;
38
53
  private listeners = new Map<string, Set<(payload: unknown) => void>>();
39
54
  private geoWatchId: number | null = null;
40
55
  private motionHandler: ((e: DeviceMotionEvent) => void) | null = null;
@@ -48,29 +63,29 @@ export class WebAdapter implements NativeKitAdapter {
48
63
  }
49
64
 
50
65
  async handshake(): Promise<Handshake> {
51
- const n = navigator as any;
66
+ const n = navigator;
52
67
  const caps: Record<string, Capability> = {
53
68
  haptics: 'vibrate' in navigator ? 'web' : 'none',
54
69
  share: 'share' in navigator ? 'web' : 'none',
55
70
  shareFiles: typeof n.canShare === 'function' ? 'web' : 'none', // navigator.canShare({files}) gates at call time
56
- orientation: (screen as any)?.orientation ? 'web' : 'none', // Screen Orientation API (lock needs fullscreen)
71
+ orientation: screen?.orientation ? 'web' : 'none', // Screen Orientation API (lock needs fullscreen)
57
72
  storage: 'web',
58
73
  secureStorage: 'none',
59
74
  // OPFS (navigator.storage.getDirectory) backs read/write/list; pickFile falls back to
60
75
  // <input type=file> even without OPFS, so 'web' whenever either surface exists.
61
- fs: n.storage?.getDirectory || typeof document !== 'undefined' ? 'web' : 'none',
76
+ fs: typeof n.storage?.getDirectory === 'function' || typeof document !== 'undefined' ? 'web' : 'none',
62
77
  toast: 'web',
63
78
  statusBar: 'none',
64
79
  device: 'web',
65
80
  clipboard: navigator.clipboard ? 'web' : 'none',
66
81
  notifications: 'Notification' in window ? 'web' : 'none',
67
- badge: typeof (navigator as any).setAppBadge === 'function' ? 'web' : 'none', // Badging API (PWA icon badge)
82
+ badge: typeof navigator.setAppBadge === 'function' ? 'web' : 'none', // Badging API (PWA icon badge)
68
83
  biometrics: 'none',
69
84
  geo: 'geolocation' in navigator ? 'web' : 'none',
70
85
  photos: 'web',
71
86
  network: 'web',
72
87
  screen: 'wakeLock' in n ? 'web' : 'none',
73
- keyboard: (window as any).visualViewport ? 'web' : 'none', // VisualViewport resize ≈ keyboard show/hide
88
+ keyboard: window.visualViewport ? 'web' : 'none', // VisualViewport resize ≈ keyboard show/hide
74
89
  dialogs: 'web',
75
90
  reviews: 'none',
76
91
  themeColor: 'web', // the browser honors <meta name="theme-color"> itself
@@ -78,14 +93,14 @@ export class WebAdapter implements NativeKitAdapter {
78
93
  contacts: n.contacts?.select ? 'web' : 'none',
79
94
  calendar: 'none',
80
95
  camera: 'web', // <input capture> — mobile browsers open the camera
81
- media: (navigator as any).mediaDevices?.getUserMedia ? 'web' : 'none',
96
+ media: typeof navigator.mediaDevices?.getUserMedia === 'function' ? 'web' : 'none',
82
97
  // BarcodeDetector (Chrome/Android) decodes; needs a getUserMedia stream to feed it. Both → 'web'.
83
- scanner: typeof (window as any).BarcodeDetector !== 'undefined' && (navigator as any).mediaDevices?.getUserMedia ? 'web' : 'none',
98
+ scanner: typeof window.BarcodeDetector !== 'undefined' && typeof navigator.mediaDevices?.getUserMedia === 'function' ? 'web' : 'none',
84
99
  // TTS: SpeechSynthesis API (broad support). STT: SpeechRecognition (Chrome/webkit only).
85
- speech: typeof (window as any).speechSynthesis !== 'undefined' ? 'web' : 'none',
100
+ speech: typeof window.speechSynthesis !== 'undefined' ? 'web' : 'none',
86
101
  speechRecognition:
87
- typeof (window as any).SpeechRecognition !== 'undefined' ||
88
- typeof (window as any).webkitSpeechRecognition !== 'undefined'
102
+ typeof window.SpeechRecognition !== 'undefined' ||
103
+ typeof window.webkitSpeechRecognition !== 'undefined'
89
104
  ? 'web'
90
105
  : 'none',
91
106
  app: 'web', // openUrl via window.open; openSettings/canOpenUrl unsupported
@@ -111,6 +126,8 @@ export class WebAdapter implements NativeKitAdapter {
111
126
  }
112
127
 
113
128
  async invoke<T>(method: string, params?: unknown): Promise<T> {
129
+ // no type: untyped JSON-wire param bag dispatched dynamically — fields flow into ~50 typed sinks
130
+ // (String()/Notification/etc.); `unknown` would force a cast at every read for zero real safety.
114
131
  const p = (params ?? {}) as Record<string, any>;
115
132
  switch (method) {
116
133
  case 'haptics.impact':
@@ -170,7 +187,7 @@ export class WebAdapter implements NativeKitAdapter {
170
187
  case 'ui.brightness.set':
171
188
  throw new KitError('UNSUPPORTED', 'No brightness control on web');
172
189
  case 'ui.keepAwake': {
173
- const wl = (navigator as any).wakeLock;
190
+ const wl = navigator.wakeLock;
174
191
  if (!wl) throw new KitError('UNSUPPORTED', 'Wake Lock API unavailable');
175
192
  if (p.on) this.wakeLock = await wl.request('screen');
176
193
  else { await this.wakeLock?.release(); this.wakeLock = null; }
@@ -197,7 +214,7 @@ export class WebAdapter implements NativeKitAdapter {
197
214
  }
198
215
 
199
216
  case 'screen.orientation.lock': {
200
- const so = (screen as any)?.orientation;
217
+ const so = screen?.orientation;
201
218
  if (!so?.lock) throw new KitError('UNSUPPORTED', 'Screen orientation lock unavailable');
202
219
  // Most browsers require fullscreen before locking; surface the real reason.
203
220
  await so.lock(toWebOrientation(String(p.orientation))).catch((e: Error) => {
@@ -206,10 +223,10 @@ export class WebAdapter implements NativeKitAdapter {
206
223
  return undefined as T;
207
224
  }
208
225
  case 'screen.orientation.unlock':
209
- (screen as any)?.orientation?.unlock?.();
226
+ screen?.orientation?.unlock?.();
210
227
  return undefined as T;
211
228
  case 'screen.orientation.current':
212
- return (((screen as any)?.orientation?.type ?? '').startsWith('landscape') ? 'landscape' : 'portrait') as T;
229
+ return ((screen?.orientation?.type ?? '').startsWith('landscape') ? 'landscape' : 'portrait') as T;
213
230
 
214
231
  case 'keyboard.hide':
215
232
  // No programmatic IME on web — blurring the focused field dismisses it.
@@ -220,7 +237,7 @@ export class WebAdapter implements NativeKitAdapter {
220
237
  throw new KitError('UNSUPPORTED', 'No in-app review prompt on web');
221
238
 
222
239
  case 'device.info': {
223
- const battery = await (navigator as any).getBattery?.().catch(() => null);
240
+ const battery = await navigator.getBattery?.().catch(() => null);
224
241
  return {
225
242
  model: navigator.userAgent.replace(/^Mozilla\/\d+\.\d+\s*/, '').slice(0, 64),
226
243
  os: 'web',
@@ -252,7 +269,7 @@ export class WebAdapter implements NativeKitAdapter {
252
269
  case 'notifications.pending':
253
270
  return 0 as T; // web timers aren't introspectable
254
271
  case 'notifications.setBadge': {
255
- const setAppBadge = (navigator as any).setAppBadge?.bind(navigator);
272
+ const setAppBadge = navigator.setAppBadge?.bind(navigator);
256
273
  if (!setAppBadge) throw new KitError('UNSUPPORTED', 'Badging API unavailable');
257
274
  await setAppBadge(p.count);
258
275
  return undefined as T;
@@ -293,7 +310,7 @@ export class WebAdapter implements NativeKitAdapter {
293
310
  case 'motion.start': {
294
311
  if (typeof DeviceMotionEvent === 'undefined') throw new KitError('UNSUPPORTED', 'No motion sensors on this browser');
295
312
  // iOS Safari gates motion behind an explicit permission prompt
296
- const req = (DeviceMotionEvent as any).requestPermission;
313
+ const req = (DeviceMotionEvent as unknown as DeviceMotionEventWithPermission).requestPermission;
297
314
  if (req) {
298
315
  const state = await req().catch((e: Error) => { throw new KitError('DENIED', e.message); });
299
316
  if (state !== 'granted') throw new KitError('DENIED', 'Motion permission not granted');
@@ -323,9 +340,9 @@ export class WebAdapter implements NativeKitAdapter {
323
340
  return undefined as T;
324
341
 
325
342
  case 'contacts.pick': {
326
- const select = (navigator as any).contacts?.select;
343
+ const select = navigator.contacts?.select;
327
344
  if (!select) throw new KitError('UNSUPPORTED', 'Contact Picker API unavailable');
328
- const picked = await (navigator as any).contacts
345
+ const picked = await navigator.contacts!
329
346
  .select(['name', 'tel', 'email'])
330
347
  .catch((e: Error) => { throw new KitError('DENIED', e.message); });
331
348
  const c = picked?.[0];
@@ -348,7 +365,7 @@ export class WebAdapter implements NativeKitAdapter {
348
365
  case 'speech.speak':
349
366
  return this.speak(String(p.text ?? ''), p) as Promise<T>;
350
367
  case 'speech.stop':
351
- (window as any).speechSynthesis?.cancel?.();
368
+ window.speechSynthesis?.cancel?.();
352
369
  return undefined as T;
353
370
  case 'speech.voices':
354
371
  return this.listVoices() as Promise<T>;
@@ -387,7 +404,7 @@ export class WebAdapter implements NativeKitAdapter {
387
404
  return undefined as T; // browser owns the audio session — nothing to tune
388
405
 
389
406
  case 'network.status': {
390
- const conn = (navigator as any).connection;
407
+ const conn = navigator.connection;
391
408
  return { online: navigator.onLine, type: conn?.type ?? (navigator.onLine ? 'unknown' : 'none') } as T;
392
409
  }
393
410
 
@@ -454,14 +471,14 @@ export class WebAdapter implements NativeKitAdapter {
454
471
  };
455
472
  }
456
473
  if (event === 'screen.orientation.change') {
457
- const so = (screen as any)?.orientation;
474
+ const so = screen?.orientation;
458
475
  if (!so) return () => {};
459
476
  const fire = () => cb(String(so.type ?? '').startsWith('landscape') ? 'landscape' : 'portrait');
460
477
  so.addEventListener('change', fire);
461
478
  return () => so.removeEventListener('change', fire);
462
479
  }
463
480
  if (event === 'keyboard.show' || event === 'keyboard.hide') {
464
- const vv = (window as any).visualViewport;
481
+ const vv = window.visualViewport;
465
482
  if (!vv) return () => {};
466
483
  let shown = false;
467
484
  const onResize = () => {
@@ -521,9 +538,9 @@ export class WebAdapter implements NativeKitAdapter {
521
538
  private scanBarcode(
522
539
  formats: string | string[] | undefined,
523
540
  facingMode: 'environment' | 'user'
524
- ): Promise<{ value: string; format: string; bounds?: any } | { cancelled: true }> {
525
- const Detector = (window as any).BarcodeDetector;
526
- const getUserMedia = (navigator as any).mediaDevices?.getUserMedia?.bind((navigator as any).mediaDevices);
541
+ ): Promise<{ value: string; format: string; bounds?: { x: number; y: number; width: number; height: number } } | { cancelled: true }> {
542
+ const Detector = window.BarcodeDetector;
543
+ const getUserMedia = navigator.mediaDevices?.getUserMedia?.bind(navigator.mediaDevices);
527
544
  if (!Detector || !getUserMedia) throw new KitError('UNSUPPORTED', 'BarcodeDetector / camera unavailable in this browser');
528
545
 
529
546
  return new Promise(async (resolve, reject) => {
@@ -552,7 +569,7 @@ export class WebAdapter implements NativeKitAdapter {
552
569
  video.setAttribute('playsinline', '');
553
570
  video.muted = true;
554
571
  video.style.cssText = 'flex:1;width:100%;height:100%;object-fit:cover';
555
- (video as any).srcObject = stream;
572
+ video.srcObject = stream;
556
573
  const close = document.createElement('button');
557
574
  close.textContent = 'Cancel';
558
575
  close.style.cssText = 'position:absolute;top:max(12px,env(safe-area-inset-top));right:16px;z-index:1;background:rgba(0,0,0,.5);color:#fff;border:0;border-radius:18px;padding:8px 16px;font:15px system-ui';
@@ -581,9 +598,11 @@ export class WebAdapter implements NativeKitAdapter {
581
598
  raf = requestAnimationFrame(tick);
582
599
  };
583
600
  raf = requestAnimationFrame(tick);
584
- } catch (e: any) {
601
+ } catch (e: unknown) {
585
602
  teardown();
586
- reject(new KitError(e?.name === 'NotAllowedError' ? 'DENIED' : 'NATIVE_ERROR', e?.message ?? 'scan failed'));
603
+ const name = e instanceof Error ? e.name : '';
604
+ const message = e instanceof Error ? e.message : 'scan failed';
605
+ reject(new KitError(name === 'NotAllowedError' ? 'DENIED' : 'NATIVE_ERROR', message));
587
606
  }
588
607
  });
589
608
  }
@@ -591,29 +610,29 @@ export class WebAdapter implements NativeKitAdapter {
591
610
  // ── speech (Web Speech API) ─────────────────────────────────────────
592
611
 
593
612
  /** Speak via SpeechSynthesis; resolve when the utterance ends (or rejects on synth error). */
594
- private speak(text: string, opts: Record<string, any>): Promise<void> {
595
- const synth = (window as any).speechSynthesis;
613
+ private speak(text: string, opts: SpeakOptions): Promise<void> {
614
+ const synth = window.speechSynthesis;
596
615
  if (!synth) throw new KitError('UNSUPPORTED', 'SpeechSynthesis unavailable');
597
616
  return new Promise((resolve, reject) => {
598
- const u = new (window as any).SpeechSynthesisUtterance(text);
617
+ const u = new window.SpeechSynthesisUtterance(text);
599
618
  if (opts.lang) u.lang = opts.lang;
600
619
  if (opts.rate != null) u.rate = opts.rate;
601
620
  if (opts.pitch != null) u.pitch = opts.pitch;
602
621
  if (opts.voice) {
603
- const v = synth.getVoices().find((vc: any) => vc.voiceURI === opts.voice || vc.name === opts.voice);
622
+ const v = synth.getVoices().find((vc) => vc.voiceURI === opts.voice || vc.name === opts.voice);
604
623
  if (v) u.voice = v;
605
624
  }
606
625
  u.onend = () => resolve();
607
- u.onerror = (e: any) => reject(new KitError('NATIVE_ERROR', e?.error ?? 'speech synthesis failed'));
626
+ u.onerror = (e: SpeechSynthesisErrorEvent) => reject(new KitError('NATIVE_ERROR', e?.error ?? 'speech synthesis failed'));
608
627
  synth.speak(u);
609
628
  });
610
629
  }
611
630
 
612
631
  /** List synthesizer voices, awaiting the async `voiceschanged` event when the list is empty. */
613
632
  private listVoices(): Promise<Array<{ id: string; name: string; lang: string }>> {
614
- const synth = (window as any).speechSynthesis;
633
+ const synth = window.speechSynthesis;
615
634
  if (!synth) throw new KitError('UNSUPPORTED', 'SpeechSynthesis unavailable');
616
- const map = (vs: any[]) => vs.map((v) => ({ id: v.voiceURI ?? v.name, name: v.name, lang: v.lang }));
635
+ const map = (vs: SpeechSynthesisVoice[]) => vs.map((v) => ({ id: v.voiceURI ?? v.name, name: v.name, lang: v.lang }));
617
636
  const ready = synth.getVoices();
618
637
  if (ready.length) return Promise.resolve(map(ready));
619
638
  // Chrome populates voices asynchronously — wait once for voiceschanged, with a short fallback.
@@ -630,8 +649,8 @@ export class WebAdapter implements NativeKitAdapter {
630
649
  }
631
650
 
632
651
  /** Capture the mic via SpeechRecognition; resolve the FINAL transcript, stream partials. */
633
- private listen(opts: Record<string, any>): Promise<string> {
634
- const Rec = (window as any).SpeechRecognition || (window as any).webkitSpeechRecognition;
652
+ private listen(opts: ListenOptions): Promise<string> {
653
+ const Rec = window.SpeechRecognition || window.webkitSpeechRecognition;
635
654
  if (!Rec) throw new KitError('UNSUPPORTED', 'SpeechRecognition unavailable (Chrome only)');
636
655
  return new Promise((resolve, reject) => {
637
656
  const rec = new Rec();
@@ -648,7 +667,7 @@ export class WebAdapter implements NativeKitAdapter {
648
667
  };
649
668
  const stop = () => { try { rec.stop(); } catch { /* already stopped */ } };
650
669
  this.listenStop = stop;
651
- rec.onresult = (e: any) => {
670
+ rec.onresult = (e) => {
652
671
  let interim = '';
653
672
  let finalText = '';
654
673
  for (let i = 0; i < e.results.length; i++) {
@@ -659,7 +678,7 @@ export class WebAdapter implements NativeKitAdapter {
659
678
  best = (finalText || interim).trim();
660
679
  if (opts.partial && interim) this.emit('speech.partial', { transcript: interim.trim() });
661
680
  };
662
- rec.onerror = (e: any) => {
681
+ rec.onerror = (e) => {
663
682
  if (settled) return;
664
683
  settled = true;
665
684
  if (this.listenStop === stop) this.listenStop = null;
@@ -713,10 +732,10 @@ export class WebAdapter implements NativeKitAdapter {
713
732
  // OPFS is one private root per origin — our `dir` enum has no meaning here, so paths resolve
714
733
  // straight off the root. Slash-separated paths walk nested dirs.
715
734
 
716
- private async opfsRoot(): Promise<any> {
717
- const dir = (navigator as any).storage?.getDirectory;
735
+ private async opfsRoot(): Promise<FileSystemDirectoryHandle> {
736
+ const dir = navigator.storage?.getDirectory;
718
737
  if (!dir) throw new KitError('UNSUPPORTED', 'No OPFS in this browser');
719
- return (navigator as any).storage.getDirectory();
738
+ return navigator.storage.getDirectory();
720
739
  }
721
740
 
722
741
  /** Split a path into clean segments, rejecting any `..` so it can't escape the OPFS root. */
@@ -727,7 +746,7 @@ export class WebAdapter implements NativeKitAdapter {
727
746
  }
728
747
 
729
748
  /** Walk `path` to its parent dir handle + leaf name. `create` makes intermediate dirs. */
730
- private async opfsResolveParent(path: string, create: boolean): Promise<{ parent: any; name: string }> {
749
+ private async opfsResolveParent(path: string, create: boolean): Promise<{ parent: FileSystemDirectoryHandle; name: string }> {
731
750
  const parts = this.opfsSegments(path);
732
751
  const name = parts.pop();
733
752
  if (!name) throw new KitError('NATIVE_ERROR', 'Empty path');
@@ -764,7 +783,7 @@ export class WebAdapter implements NativeKitAdapter {
764
783
  let dir = await this.opfsRoot();
765
784
  for (const seg of this.opfsSegments(path)) dir = await dir.getDirectoryHandle(seg);
766
785
  const out: Array<{ name: string; type: 'file' | 'dir' }> = [];
767
- for await (const [name, handle] of (dir as any).entries()) {
786
+ for await (const [name, handle] of dir.entries()) {
768
787
  out.push({ name, type: handle.kind === 'directory' ? 'dir' : 'file' });
769
788
  }
770
789
  return out;
@@ -14,8 +14,8 @@ export interface HttpJsonOptions {
14
14
  }
15
15
 
16
16
  /** POST/GET JSON, throwing on non-2xx. Keeps validators + providers DRY. */
17
- export async function httpJson(opts: HttpJsonOptions): Promise<any> {
18
- const f = opts.fetch ?? (globalThis as any).fetch;
17
+ export async function httpJson(opts: HttpJsonOptions): Promise<unknown> {
18
+ const f = opts.fetch ?? globalThis.fetch;
19
19
  if (!f) throw new Error('billing: no fetch available — pass one via options.fetch');
20
20
  const h = typeof opts.headers === 'function' ? await opts.headers() : opts.headers;
21
21
  const res = await f(opts.url, {
@@ -13,8 +13,10 @@ export class HttpBillingProviderOptions {
13
13
  redirect: (url: string) => void = (url) => {
14
14
  if (typeof window !== 'undefined') window.location.assign(url);
15
15
  };
16
- mapProducts: (json: any) => Product[] = (j) => (j?.products ?? j ?? []) as Product[];
17
- mapEntitlements: (json: any) => Entitlement[] = (j) => (j?.entitlements ?? j ?? []) as Entitlement[];
16
+ mapProducts: (json: unknown) => Product[] = (j) =>
17
+ ((j as { products?: Product[] } | null)?.products ?? (j as Product[] | null) ?? []);
18
+ mapEntitlements: (json: unknown) => Entitlement[] = (j) =>
19
+ ((j as { entitlements?: Entitlement[] } | null)?.entitlements ?? (j as Entitlement[] | null) ?? []);
18
20
  }
19
21
 
20
22
  /**
@@ -39,8 +41,9 @@ export class HttpBillingProvider implements BillingProvider {
39
41
 
40
42
  async purchase(productId: string): Promise<Entitlement[]> {
41
43
  const json = await httpJson({ ...this.req(), url: `${this.base()}/checkout`, method: 'POST', body: { productId } });
42
- if (json?.url) {
43
- this.options.redirect(String(json.url)); // hosted checkout — page navigates away
44
+ const url = (json as { url?: unknown } | null)?.url;
45
+ if (url) {
46
+ this.options.redirect(String(url)); // hosted checkout — page navigates away
44
47
  return []; // entitlements arrive via entitlements() after redirect-back
45
48
  }
46
49
  return this.options.mapEntitlements(json); // backend completed inline
@@ -56,7 +59,8 @@ export class HttpBillingProvider implements BillingProvider {
56
59
 
57
60
  async manageSubscriptions(): Promise<void> {
58
61
  const json = await httpJson({ ...this.req(), url: `${this.base()}/portal`, method: 'POST', body: {} });
59
- if (json?.url) this.options.redirect(String(json.url)); // Stripe Billing Portal, etc.
62
+ const url = (json as { url?: unknown } | null)?.url;
63
+ if (url) this.options.redirect(String(url)); // Stripe Billing Portal, etc.
60
64
  }
61
65
 
62
66
  private base() {
@@ -32,7 +32,8 @@ export class HttpValidatorOptions {
32
32
  /** Static headers or a thunk (e.g. to inject a fresh bearer token). */
33
33
  headers?: HeaderProvider;
34
34
  /** Map the provider's response JSON → `Entitlement[]`. Default expects `{ entitlements: [...] }`. */
35
- mapResponse: (json: any) => Entitlement[] = (j) => (j?.entitlements ?? []) as Entitlement[];
35
+ mapResponse: (json: unknown) => Entitlement[] = (j) =>
36
+ ((j as { entitlements?: Entitlement[] } | null)?.entitlements ?? []);
36
37
  /** Injected for tests / non-DOM runtimes. Defaults to global `fetch`. */
37
38
  fetch?: typeof fetch;
38
39
  }
@@ -156,7 +156,7 @@ export class UpdatesModule {
156
156
  * `<meta name="app-version">` tag. '' (unknown) when neither is present — in which case we never
157
157
  * raise a false update prompt. */
158
158
  function bootVersion(): string {
159
- const g = typeof window !== 'undefined' ? (window as any).__APP_VERSION__ : undefined;
159
+ const g = typeof window !== 'undefined' ? (window as { __APP_VERSION__?: string }).__APP_VERSION__ : undefined;
160
160
  if (g) return String(g);
161
161
  if (typeof document !== 'undefined') {
162
162
  const meta = document.querySelector('meta[name="app-version"]');
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Ambient augmentations for the non-standard / experimental Web APIs the WebAdapter
3
+ * feature-detects. These are NOT in the current TS DOM lib, so without these decls
4
+ * each touch would need an `as any` cast. Shapes are intentionally MINIMAL — only the
5
+ * members web-adapter.ts actually reads — but REAL (no `any`), so call sites stay typed.
6
+ *
7
+ * Anything the DOM lib already types (setAppBadge/clearAppBadge, speechSynthesis,
8
+ * SpeechSynthesisUtterance, visualViewport, mediaDevices, wakeLock, ScreenOrientation
9
+ * core, storage.getDirectory, HTMLVideoElement.playsInline/srcObject) is deliberately
10
+ * absent here and used straight off the lib types.
11
+ */
12
+
13
+ export {};
14
+
15
+ /** A picked contact from the Contact Picker API (navigator.contacts.select). */
16
+ interface WebContact {
17
+ name?: string[];
18
+ tel?: string[];
19
+ email?: string[];
20
+ }
21
+
22
+ /** A detected code from the Barcode Detection API. */
23
+ interface DetectedBarcode {
24
+ rawValue: string;
25
+ format: string;
26
+ boundingBox?: { x: number; y: number; width: number; height: number };
27
+ }
28
+
29
+ /** Minimal BarcodeDetector surface (Chrome/Android). */
30
+ interface BarcodeDetectorLike {
31
+ detect(source: CanvasImageSource): Promise<DetectedBarcode[]>;
32
+ }
33
+ interface BarcodeDetectorCtor {
34
+ new (options?: { formats?: string[] }): BarcodeDetectorLike;
35
+ }
36
+
37
+ /** Minimal Web Speech `SpeechRecognition` surface (Chrome/webkit). */
38
+ interface SpeechRecognitionLike {
39
+ lang: string;
40
+ interimResults: boolean;
41
+ continuous: boolean;
42
+ onresult: ((event: { results: ArrayLike<ArrayLike<{ transcript: string }> & { isFinal: boolean }> }) => void) | null;
43
+ onerror: ((event: { error?: string }) => void) | null;
44
+ onend: (() => void) | null;
45
+ start(): void;
46
+ stop(): void;
47
+ }
48
+ interface SpeechRecognitionCtor {
49
+ new (): SpeechRecognitionLike;
50
+ }
51
+
52
+ declare global {
53
+ interface Navigator {
54
+ /** Battery Status API — resolves a snapshot the adapter reads `level`/`charging` off. */
55
+ getBattery?(): Promise<{ level: number; charging: boolean }>;
56
+ /** Contact Picker API. */
57
+ contacts?: {
58
+ select(properties: string[], options?: { multiple?: boolean }): Promise<WebContact[]>;
59
+ };
60
+ /** Network Information API (non-standard `connection`). */
61
+ connection?: { type?: string };
62
+ }
63
+
64
+ interface Window {
65
+ BarcodeDetector?: BarcodeDetectorCtor;
66
+ SpeechRecognition?: SpeechRecognitionCtor;
67
+ webkitSpeechRecognition?: SpeechRecognitionCtor;
68
+ }
69
+
70
+ /** Screen Orientation lock/unlock — newer than the lib's ScreenOrientation interface. */
71
+ interface ScreenOrientation {
72
+ lock?(orientation: string): Promise<void>;
73
+ unlock?(): void;
74
+ }
75
+
76
+ /** OPFS directory async iterator — newer than the lib's FileSystemDirectoryHandle interface. */
77
+ interface FileSystemDirectoryHandle {
78
+ entries(): AsyncIterableIterator<[string, FileSystemHandle]>;
79
+ }
80
+
81
+ /** iOS Safari gates DeviceMotion behind an explicit permission prompt (static method) — the lib
82
+ * already declares `DeviceMotionEvent` as a `var`, so this can't merge onto it; web-adapter casts
83
+ * the constructor to this shape at the single call site instead. */
84
+ interface DeviceMotionEventWithPermission {
85
+ requestPermission?(): Promise<'granted' | 'denied' | 'prompt'>;
86
+ }
87
+ }