@livx.cc/appwrap 0.48.0 → 0.48.2

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/appwrap",
3
- "version": "0.48.0",
3
+ "version": "0.48.2",
4
4
  "description": "Wrap any PWA into a native app with native capabilities (appwrap runtime + @livx.cc/native-kit).",
5
5
  "license": "MIT",
6
6
  "author": "Elya Livshitz",
@@ -98,6 +98,16 @@ export const MODULES: ModuleManifest[] = [
98
98
  ios: { permissions: [{ key: 'NSLocationWhenInUseUsageDescription', domain: 'location', defaultUsage: 'Show your location.' }] },
99
99
  android: { permissions: ['android.permission.ACCESS_FINE_LOCATION', 'android.permission.ACCESS_COARSE_LOCATION'] },
100
100
  },
101
+ // heading (compass) — handlers live in the always-bundled parity/android groups (registerParity/
102
+ // AndroidHandlers). iOS CLHeading via CLLocationManager: trueHeading needs location auth, so it rides
103
+ // geo's SAME NSLocationWhenInUseUsageDescription (the CLI dedups when geo is also active; declared here
104
+ // for self-containment so a heading-only app still stamps it). Android uses TYPE_ROTATION_VECTOR — a
105
+ // motion sensor that needs NO runtime permission, so no android block (avoid an unused-permission flag).
106
+ {
107
+ name: 'heading', group: 'parity',
108
+ capabilities: { heading: 'native' },
109
+ ios: { permissions: [{ key: 'NSLocationWhenInUseUsageDescription', domain: 'location', defaultUsage: 'Show your compass heading.' }] },
110
+ },
101
111
  {
102
112
  name: 'photos', group: 'extended',
103
113
  capabilities: { photos: 'native' },
@@ -308,39 +318,11 @@ export const MODULES: ModuleManifest[] = [
308
318
  },
309
319
 
310
320
  // ── widget — home-screen presence via a native extension (iOS WidgetKit / Android AppWidget) ──
311
- // Opt-in; ships its own extension target under modules-native/widget/. The host publishes tiles into
312
- // an App Group container the extension reads at render; each tile deep-links back on tap. The App
313
- // Group entitlement is app-specific (group.<appId>), so consumers supply it via appwrap.config
314
- // `iosEntitlements` for now (Phase-2 templates `group.$APPID` in the CLI). Phase-1 spike ships a
315
- // single static tile to prove the extension builds, provisions the group, renders, and deep-links.
316
- {
317
- name: 'widget', group: 'widget',
318
- capabilities: { widget: { ios: true, android: true } },
319
- // Android launcher widget = an AppWidgetProvider receiver (icon-grid) fed by the widget.publish
320
- // handler via SharedPreferences; Kotlin source + res ship as nativeSrc. iOS side = the WidgetKit
321
- // extension under nativeSrc's App_Resources/iOS/extensions/.
322
- android: {
323
- kotlin: true,
324
- manifestApplication: `<receiver
325
- android:name="cc.livx.appwrap.AppwrapWidgetProvider"
326
- android:exported="true">
327
- <intent-filter>
328
- <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
329
- </intent-filter>
330
- <meta-data
331
- android:name="android.appwidget.provider"
332
- android:resource="@xml/appwrap_widget_info" />
333
- </receiver>`,
334
- },
335
- nativeSrc: 'widget',
336
- },
337
-
338
- // ── widget — home-screen presence via a native extension (iOS WidgetKit / Android AppWidget) ──
339
- // Opt-in; ships its own extension target under modules-native/widget/. The host publishes tiles into
340
- // an App Group container the extension reads at render; each tile deep-links back on tap. The App
341
- // Group entitlement is app-specific (group.<appId>), so consumers supply it via appwrap.config
342
- // `iosEntitlements` for now (Phase-2 templates `group.$APPID` in the CLI). Phase-1 spike ships a
343
- // single static tile to prove the extension builds, provisions the group, renders, and deep-links.
321
+ // Opt-in; ships its own native surfaces under modules-native/widget/. The app publishes tiles/stats
322
+ // into a shared container (App Group on iOS, SharedPreferences on Android) via the `widget.publish`
323
+ // handler; the extension/provider reads it at render and each tile deep-links back on tap. Two kinds:
324
+ // a `launcher` icon-grid (the host's pinned apps) and a `data` widget (a mini-app's live stat/list).
325
+ // The App Group id is derived (`group.<appId>`) and templated into both targets by the CLI.
344
326
  {
345
327
  name: 'widget', group: 'widget',
346
328
  capabilities: { widget: { ios: true, android: true } },
@@ -365,6 +365,44 @@ export function registerAndroidHandlers(): void {
365
365
  motionListener = null;
366
366
  });
367
367
 
368
+ // ── heading (SensorManager: rotation vector → compass azimuth) ─────
369
+ // TYPE_ROTATION_VECTOR fuses accelerometer+magnetometer(+gyro) into an orientation; getOrientation
370
+ // gives azimuth (rad, counter-clockwise from north), which we convert to a 0–360 compass heading.
371
+ // UNVERIFIED-ON-DEVICE.
372
+ let headingListener: android.hardware.SensorEventListener | null = null;
373
+
374
+ bridge.register('heading.start', (p: { hz?: number } = {}) => {
375
+ if (headingListener) return; // already streaming
376
+ const hz = Math.max(1, Math.min(60, p.hz || 10));
377
+ const minMs = 1000 / hz;
378
+ const sm = sensorManager();
379
+ const rot = sm.getDefaultSensor(android.hardware.Sensor.TYPE_ROTATION_VECTOR);
380
+ if (!rot) throw err('UNSUPPORTED', 'No rotation-vector sensor (compass) on this device');
381
+ const R = Array.create('float', 9);
382
+ const orientation = Array.create('float', 3);
383
+ let last = 0;
384
+ headingListener = new android.hardware.SensorEventListener({
385
+ onAccuracyChanged() {},
386
+ onSensorChanged(event: android.hardware.SensorEvent) {
387
+ const now = java.lang.System.currentTimeMillis();
388
+ if (now - last < minMs) return; // throttle emit to the requested Hz
389
+ last = now;
390
+ android.hardware.SensorManager.getRotationMatrixFromVector(R, event.values);
391
+ android.hardware.SensorManager.getOrientation(R, orientation);
392
+ // orientation[0] = azimuth in rad (−π..π), CCW from north. Convert to 0–360 compass degrees.
393
+ const deg = ((orientation[0] * 180) / Math.PI + 360) % 360;
394
+ bridge.emit('heading.data', { deg });
395
+ },
396
+ });
397
+ const periodUs = Math.max(5000, Math.round(1_000_000 / hz));
398
+ sm.registerListener(headingListener, rot, periodUs);
399
+ });
400
+
401
+ bridge.register('heading.stop', () => {
402
+ if (headingListener) sensorManager().unregisterListener(headingListener);
403
+ headingListener = null;
404
+ });
405
+
368
406
  // ── contacts (ACTION_PICK + ContactsContract query) ────────────────
369
407
  bridge.register('contacts.pick', async () => {
370
408
  if (!(await requestPermissions(['android.permission.READ_CONTACTS']))) {
@@ -24,7 +24,7 @@ function readContact(contact: CNContact): { name: string; phones: string[]; emai
24
24
  // shared module can instantiate on Android — NSObject/CL*/CN*/UIImagePicker* are iOS globals, and a
25
25
  // top-level `extends NSObject` would evaluate at ES-module load and crash the Android shell.
26
26
  // any: module-level holders for runtime-built ObjC subclasses; each class BODY stays fully typed.
27
- let GeoWatchDelegate: any, ContactPickerDelegate: any, CameraCaptureDelegate: any;
27
+ let GeoWatchDelegate: any, HeadingDelegate: any, ContactPickerDelegate: any, CameraCaptureDelegate: any;
28
28
  function ensureIosDelegates(): void {
29
29
  if (!isIOS || GeoWatchDelegate) return;
30
30
 
@@ -58,6 +58,30 @@ function ensureIosDelegates(): void {
58
58
  }
59
59
  GeoWatchDelegate = GeoWatchDelegateImpl;
60
60
 
61
+ // CLLocationManager delegate for the compass heading stream (heading.watch). Emits a 0–360
62
+ // compass heading; prefers trueHeading (needs location auth) and falls back to magneticHeading.
63
+ // UNVERIFIED-ON-DEVICE.
64
+ @NativeClass()
65
+ class HeadingDelegateImpl extends NSObject implements CLLocationManagerDelegate {
66
+ static ObjCProtocols = [CLLocationManagerDelegate];
67
+ static new(): HeadingDelegateImpl {
68
+ return <HeadingDelegateImpl>super.new();
69
+ }
70
+ locationManagerDidUpdateHeading(_m: CLLocationManager, heading: CLHeading): void {
71
+ // trueHeading is negative when unavailable (no location auth / no recent fix) — magneticHeading
72
+ // is always valid once the magnetometer is calibrated.
73
+ const deg = heading.trueHeading >= 0 ? heading.trueHeading : heading.magneticHeading;
74
+ bridge.emit('heading.data', {
75
+ deg: ((deg % 360) + 360) % 360,
76
+ accuracy: heading.headingAccuracy >= 0 ? heading.headingAccuracy : undefined,
77
+ });
78
+ }
79
+ locationManagerDidFailWithError(_m: CLLocationManager, error: NSError): void {
80
+ console.warn('AppWrap: heading.watch error', error.localizedDescription);
81
+ }
82
+ }
83
+ HeadingDelegate = HeadingDelegateImpl;
84
+
61
85
  // CNContactPickerViewController delegate. The picker resolution callbacks are set as instance fields
62
86
  // after construction (replacing the old closure-captured `resolve`).
63
87
  @NativeClass()
@@ -220,6 +244,36 @@ export function registerParityHandlers(): void {
220
244
  motionManager = null;
221
245
  });
222
246
 
247
+ // ── heading (CLLocationManager compass) ────────────────────────────
248
+ // Compass heading via CLLocationManager.startUpdatingHeading (delegate-pushed, not polled — the
249
+ // heading callback is reliable, unlike CoreMotion's block). UNVERIFIED-ON-DEVICE.
250
+ let headingManager: CLLocationManager | null = null;
251
+ let headingDelegate: any = null;
252
+
253
+ bridge.register('heading.start', () => {
254
+ if (!isIOS) throw iosOnly();
255
+ if (headingManager) return; // already streaming
256
+ Utils.dispatchToMainThread(() => {
257
+ const mm = CLLocationManager.new();
258
+ if (!CLLocationManager.headingAvailable()) throw err('UNSUPPORTED', 'No compass on this device');
259
+ headingManager = mm;
260
+ headingDelegate = HeadingDelegate.new();
261
+ mm.delegate = headingDelegate;
262
+ mm.headingFilter = 1; // emit on ≥1° change
263
+ // trueHeading requires location auth; request it so we can report true (not just magnetic) north.
264
+ if (mm.authorizationStatus === CLAuthorizationStatus.kCLAuthorizationStatusNotDetermined) {
265
+ mm.requestWhenInUseAuthorization();
266
+ }
267
+ mm.startUpdatingHeading();
268
+ });
269
+ });
270
+
271
+ bridge.register('heading.stop', () => {
272
+ headingManager?.stopUpdatingHeading();
273
+ headingManager = null;
274
+ headingDelegate = null;
275
+ });
276
+
223
277
  // ── contacts (CNContactPickerViewController — no permission needed) ──
224
278
  bridge.register('contacts.pick', () => {
225
279
  if (!isIOS) throw iosOnly();
@@ -37,6 +37,24 @@ window.__APPWRAP_DEBUG__=${SHELL_CONFIG.debug ? 'true' : 'false'};
37
37
  });
38
38
  window.addEventListener('error', function(e){ post('[uncaught] ' + (e.message || e) + ' @ ' + (e.filename || '') + ':' + (e.lineno || 0)); });
39
39
  window.addEventListener('unhandledrejection', function(e){ var r = e.reason; post('[rejection] ' + ((r && (r.stack || r.message)) || r)); });
40
+ // Debug builds only: a loud, non-interactive DEV strip pinned to the top edge showing the server
41
+ // this shell points at. A dev-pointing ipa (debug:true) must never be mistaken for a store/prod
42
+ // build — the host in the strip makes "which backend am I on?" answerable at a glance.
43
+ var srv = ${JSON.stringify(SHELL_CONFIG.serverUrl || '')};
44
+ var host = ''; try { host = srv ? new URL(srv).host : (location && location.host) || ''; } catch (e) { host = srv || ''; }
45
+ function mountDevStrip(){
46
+ if (document.getElementById('__appwrap_dev_strip__') || !document.body) return;
47
+ var el = document.createElement('div');
48
+ el.id = '__appwrap_dev_strip__';
49
+ el.textContent = 'DEV · ' + host;
50
+ el.style.cssText = 'position:fixed;top:0;left:0;right:0;z-index:2147483647;pointer-events:none;'
51
+ + 'background:#b91c1c;color:#fff;text-align:center;letter-spacing:.5px;'
52
+ + 'font:600 10px/1 -apple-system,system-ui,sans-serif;'
53
+ + 'padding:calc(env(safe-area-inset-top,0px) + 2px) 6px 2px;';
54
+ document.body.appendChild(el);
55
+ }
56
+ if (document.body) mountDevStrip();
57
+ else document.addEventListener('DOMContentLoaded', mountDevStrip);
40
58
  })();`;
41
59
 
42
60
  /**