@livx.cc/appwrap 0.61.8 → 0.61.10

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.61.8",
3
+ "version": "0.61.10",
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",
@@ -8,6 +8,7 @@ import { setStatusBarStyle } from './status-bar';
8
8
  import { buildCapabilityMap } from './capabilities.manifest';
9
9
  import { ACTIVE_MODULE_NAMES, PACK_MODULES } from './active-modules.generated';
10
10
  import { consumePendingBackgroundTaskId } from './background-context';
11
+ import { settleShare, type ShareOutcome } from './share-outcome';
11
12
 
12
13
  /** Build identifier for the native shell bundle — bump per deploy to spot stale bundles. */
13
14
  export const SHELL_BUILD = 'share-target-1';
@@ -18,6 +19,40 @@ let lastWebVersion: WebVersionInfo = {};
18
19
  /** Latest version status the web reported — read by the dev-menu App Info screen. */
19
20
  export function getReportedWebVersion(): WebVersionInfo { return lastWebVersion; }
20
21
 
22
+ /**
23
+ * Present the iOS share sheet and resolve when it ENDS — not when it opens — with how it ended
24
+ * (`{ completed, activity? }`, e.g. activity `com.apple.UIKit.activity.SaveToCameraRoll` for "Save
25
+ * Video"). That is what lets a caller say "Saved to Photos"; the sheet itself gives no feedback.
26
+ */
27
+ function presentShareSheet(items: NSMutableArray<any>): Promise<ShareOutcome> {
28
+ return new Promise((resolve) => {
29
+ const controller = UIActivityViewController.alloc().initWithActivityItemsApplicationActivities(items, null);
30
+ const rootVC = Utils.ios.getRootViewController();
31
+ // iPad requires a popover anchor
32
+ if (controller.popoverPresentationController) {
33
+ controller.popoverPresentationController.sourceView = rootVC.view;
34
+ }
35
+ let done = false;
36
+ const finish = (o: ShareOutcome | null) => {
37
+ if (done || !o) return;
38
+ done = true;
39
+ resolve(o);
40
+ };
41
+ const presented = () => !!controller.presentingViewController && !controller.beingDismissed;
42
+ controller.completionWithItemsHandler = (activityType: string, completed: boolean) => {
43
+ // Recover the WebView on dismiss — a presented sheet can orphan a touch-stealing window / leave
44
+ // the renderer throttled (see CustomWebView.recoverAfterNativeSurface).
45
+ bridge.getWebView()?.recoverAfterNativeSurface();
46
+ const now = settleShare(activityType, !!completed, presented());
47
+ if (now) return finish(now);
48
+ // Still presented: a cancelled sub-activity (sheet stays) OR a dismissal still animating out.
49
+ // Re-check once it would have finished; a later handler call wins if it comes first.
50
+ setTimeout(() => finish(settleShare(null, false, presented())), 700);
51
+ };
52
+ rootVC.presentViewControllerAnimatedCompletion(controller, true, null);
53
+ });
54
+ }
55
+
21
56
  /** Register all protocol-v1 handlers. */
22
57
  export function registerHandlers(): void {
23
58
  bridge.register('app.handshake', () => {
@@ -84,20 +119,10 @@ export function registerHandlers(): void {
84
119
  if (isIOS) {
85
120
  const items = NSMutableArray.new();
86
121
  if (content) items.addObject(content);
87
- const controller = UIActivityViewController.alloc().initWithActivityItemsApplicationActivities(
88
- items,
89
- null
90
- );
91
- const rootVC = Utils.ios.getRootViewController();
92
- // iPad requires a popover anchor
93
- if (controller.popoverPresentationController) {
94
- controller.popoverPresentationController.sourceView = rootVC.view;
95
- }
96
- // Recover the WebView on dismiss — a presented sheet can orphan a touch-stealing window / leave
97
- // the renderer throttled (see CustomWebView.recoverAfterNativeSurface).
98
- controller.completionWithItemsHandler = () => { bridge.getWebView()?.recoverAfterNativeSurface(); };
99
- rootVC.presentViewControllerAnimatedCompletion(controller, true, null);
122
+ return presentShareSheet(items);
100
123
  } else if (isAndroid) {
124
+ // The chooser cannot report what (or whether) anything was picked, so there is no outcome to
125
+ // return: resolves undefined, which callers treat as "unknown" (no success feedback).
101
126
  const intent = new android.content.Intent(android.content.Intent.ACTION_SEND);
102
127
  intent.setType('text/plain');
103
128
  if (title) intent.putExtra(android.content.Intent.EXTRA_SUBJECT, title);
@@ -123,15 +148,7 @@ export function registerHandlers(): void {
123
148
  data.writeToFileAtomically(filePath, true);
124
149
  items.addObject(NSURL.fileURLWithPath(filePath));
125
150
  });
126
- const controller = UIActivityViewController.alloc().initWithActivityItemsApplicationActivities(
127
- items,
128
- null
129
- );
130
- const rootVC = Utils.ios.getRootViewController();
131
- if (controller.popoverPresentationController) {
132
- controller.popoverPresentationController.sourceView = rootVC.view; // iPad anchor
133
- }
134
- rootVC.presentViewControllerAnimatedCompletion(controller, true, null);
151
+ return presentShareSheet(items);
135
152
  }
136
153
  );
137
154
 
@@ -0,0 +1,21 @@
1
+ /** How a share sheet ended — what `share.share` / `share.files` resolve with (native-kit `ShareResult`). */
2
+ export interface ShareOutcome {
3
+ completed: boolean;
4
+ activity?: string;
5
+ }
6
+
7
+ /**
8
+ * Decide whether ONE `UIActivityViewController.completionWithItemsHandler` call ends the share.
9
+ * iOS 13+ calls the handler with (activityType, completed=false) when a sub-activity (Mail, Messages)
10
+ * is cancelled while the sheet STAYS open for another pick — resolving there would report "cancelled"
11
+ * and lose the real outcome. So: a completed activity ends it; otherwise only a sheet that is no longer
12
+ * presented does. `null` = not over yet, wait for the next call (or the dismissal re-check).
13
+ */
14
+ export function settleShare(
15
+ activityType: string | null | undefined,
16
+ completed: boolean,
17
+ sheetStillPresented: boolean
18
+ ): ShareOutcome | null {
19
+ if (completed) return activityType ? { completed: true, activity: String(activityType) } : { completed: true };
20
+ return sheetStillPresented ? null : { completed: false };
21
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * iOS share sheet outcome — `share.share` / `share.files` resolve when the sheet ENDS with
3
+ * `{ completed, activity? }`. Contract: a completed activity ends it (with its type), a dismissed sheet
4
+ * ends it as not completed, and a cancelled sub-activity while the sheet stays open does NOT end it.
5
+ */
6
+ import { describe, expect, test } from 'bun:test';
7
+ import { settleShare } from '../app/shell/share-outcome';
8
+
9
+ describe('settleShare', () => {
10
+ test('"Save Video" completes with its activity type', () => {
11
+ expect(settleShare('com.apple.UIKit.activity.SaveToCameraRoll', true, false))
12
+ .toEqual({ completed: true, activity: 'com.apple.UIKit.activity.SaveToCameraRoll' });
13
+ });
14
+ test('completed while the sheet is still animating out still completes', () => {
15
+ expect(settleShare('com.apple.UIKit.activity.CopyToPasteboard', true, true))
16
+ .toEqual({ completed: true, activity: 'com.apple.UIKit.activity.CopyToPasteboard' });
17
+ });
18
+ test('completed with no activity type carries no activity key', () => {
19
+ expect(settleShare(null, true, false)).toEqual({ completed: true });
20
+ });
21
+ test('sheet dismissed without a pick → not completed', () => {
22
+ expect(settleShare(null, false, false)).toEqual({ completed: false });
23
+ });
24
+ test('sub-activity cancelled while the sheet stays open → not over yet', () => {
25
+ expect(settleShare('com.apple.UIKit.activity.Mail', false, true)).toBeNull();
26
+ });
27
+ });