@livx.cc/appwrap 0.30.1 → 0.30.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 +1 -1
- package/runtime/app/shell/custom-webview.android.ts +6 -0
- package/runtime/app/shell/custom-webview.ios.ts +50 -0
- package/runtime/app/shell/custom-webview.ts +7 -0
- package/runtime/app/shell/handlers-billing.ts +7 -10
- package/runtime/app/shell/handlers-extended.ts +2 -0
- package/runtime/app/shell/handlers-oauth.ts +3 -0
- package/runtime/app/shell/handlers-system.ts +16 -0
- package/runtime/app/shell/handlers.ts +3 -0
- package/runtime/modules-native/billing/App_Resources/iOS/src/AppwrapManageSubscriptions.swift +13 -64
package/package.json
CHANGED
|
@@ -95,6 +95,12 @@ export class CustomWebView extends WebView {
|
|
|
95
95
|
/* iOS-only */
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
+
/** iOS-only post-native-surface recovery (see custom-webview.ios.ts). No-op on Android: dismissing a
|
|
99
|
+
* native screen returns through the normal activity resume, which already restores the WebView. */
|
|
100
|
+
recoverAfterNativeSurface(): void {
|
|
101
|
+
/* iOS-only */
|
|
102
|
+
}
|
|
103
|
+
|
|
98
104
|
private installDocumentStartShim(wv: android.webkit.WebView): void {
|
|
99
105
|
try {
|
|
100
106
|
const wkt = androidx.webkit;
|
|
@@ -113,6 +113,56 @@ export class CustomWebView extends WebView {
|
|
|
113
113
|
);
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
/** Call after dismissing ANY native surface presented over the WebView (StoreKit sheet, OAuth
|
|
117
|
+
* ASWebAuthenticationSession, SFSafariViewController, pickers, share, alerts). Such surfaces can
|
|
118
|
+
* leave the WebView frozen: a same-scene system sheet doesn't fire resumeEvent AND can orphan an
|
|
119
|
+
* interactive window above ours that swallows touches. We neutralise any stray window FIRST (so the
|
|
120
|
+
* wake re-attaches over a clean stack), then wake. Deferred so the dismissal animation finalizes the
|
|
121
|
+
* orphan window first. Idempotent + safe (no stray ⇒ no-op). No-op on Android. */
|
|
122
|
+
recoverAfterNativeSurface(): void {
|
|
123
|
+
setTimeout(() => {
|
|
124
|
+
try { this.neutralizeStrayWindows(); } catch (e) { /* best-effort */ }
|
|
125
|
+
this.wakeWebContent();
|
|
126
|
+
}, 350);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Remove any window sitting ABOVE our main app window (a leftover system-surface window that can
|
|
130
|
+
* intercept touches): disable interaction, hide, and detach from the scene. Re-key our main window.
|
|
131
|
+
* Main = lowest-level non-hidden window with a rootViewController. Logs before/after (debug). */
|
|
132
|
+
private neutralizeStrayWindows(): void {
|
|
133
|
+
const dbg = SHELL_CONFIG.debug;
|
|
134
|
+
const scenes = UIApplication.sharedApplication.connectedScenes.allObjects;
|
|
135
|
+
for (let i = 0; i < scenes.count; i++) {
|
|
136
|
+
const scene = scenes.objectAtIndex(i);
|
|
137
|
+
if (!scene.isKindOfClass(UIWindowScene.class())) continue;
|
|
138
|
+
const ws = scene as UIWindowScene;
|
|
139
|
+
const windows = ws.windows;
|
|
140
|
+
let main: UIWindow | null = null;
|
|
141
|
+
for (let j = 0; j < windows.count; j++) {
|
|
142
|
+
const w = windows.objectAtIndex(j);
|
|
143
|
+
if (w.hidden || !w.rootViewController) continue;
|
|
144
|
+
if (!main || w.windowLevel < main.windowLevel) main = w;
|
|
145
|
+
}
|
|
146
|
+
for (let j = 0; j < windows.count; j++) {
|
|
147
|
+
const w = windows.objectAtIndex(j);
|
|
148
|
+
if (main && w !== main && w.windowLevel > 0 && !w.hidden) {
|
|
149
|
+
if (dbg) appendWebLog(`[native:recover] stray win[${j}] lvl=${w.windowLevel} uie=${w.userInteractionEnabled} key=${w.keyWindow}`);
|
|
150
|
+
w.userInteractionEnabled = false;
|
|
151
|
+
w.hidden = true;
|
|
152
|
+
w.windowScene = null; // detach from the scene — strongest removal
|
|
153
|
+
if (dbg) appendWebLog(`[native:recover] after set: hidden=${w.hidden} uie=${w.userInteractionEnabled} scene=${w.windowScene ? 'set' : 'nil'}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (main) main.makeKeyAndVisible();
|
|
157
|
+
if (dbg) {
|
|
158
|
+
let s = '';
|
|
159
|
+
const after = ws.windows;
|
|
160
|
+
for (let k = 0; k < after.count; k++) { const w = after.objectAtIndex(k); s += ` [${k}]lvl${w.windowLevel}h${w.hidden}k${w.keyWindow}`; }
|
|
161
|
+
appendWebLog(`[native:recover] post mainKey=${main ? main.keyWindow : '?'} windows:${s}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
116
166
|
createNativeView(): WKWebView {
|
|
117
167
|
const config = WKWebViewConfiguration.new();
|
|
118
168
|
config.allowsInlineMediaPlayback = true;
|
|
@@ -20,4 +20,11 @@ export class CustomWebView extends WebView {
|
|
|
20
20
|
wakeWebContent(): void {
|
|
21
21
|
/* overridden per-platform */
|
|
22
22
|
}
|
|
23
|
+
|
|
24
|
+
/** Call after dismissing ANY native surface presented over the WebView (StoreKit sheet, OAuth,
|
|
25
|
+
* SFSafariViewController, pickers, share, alerts). iOS: neutralises an orphaned touch-stealing
|
|
26
|
+
* window the surface may leave + wakes the renderer (see custom-webview.ios.ts). No-op on Android. */
|
|
27
|
+
recoverAfterNativeSurface(): void {
|
|
28
|
+
/* overridden per-platform */
|
|
29
|
+
}
|
|
23
30
|
}
|
|
@@ -5,8 +5,7 @@ import { SHELL_CONFIG } from './config';
|
|
|
5
5
|
|
|
6
6
|
// @objc Swift shim (AppwrapManageSubscriptions.swift, billing nativeSrc) — no NS types; bridges
|
|
7
7
|
// StoreKit 2's Swift-async showManageSubscriptions(in:) to a completion the ObjC bridge can call.
|
|
8
|
-
|
|
9
|
-
declare const AppwrapManageSubscriptions: { present(completion: (message: string | null, diagnostic: string | null) => void): void };
|
|
8
|
+
declare const AppwrapManageSubscriptions: { present(completion: (message: string | null) => void): void };
|
|
10
9
|
|
|
11
10
|
/**
|
|
12
11
|
* In-app purchases — iOS StoreKit 1 (SKPaymentQueue). StoreKit 2 (Product/Transaction)
|
|
@@ -217,14 +216,12 @@ export function registerBillingHandlers(): void {
|
|
|
217
216
|
// deep-link path above is untouched. Rejects on iOS <15, no scene, or a StoreKit error.
|
|
218
217
|
bridge.register('billing.manageSubscriptionsSheet', () => {
|
|
219
218
|
return new Promise<void>((resolve, reject) => {
|
|
220
|
-
AppwrapManageSubscriptions.present((message: string | null
|
|
221
|
-
if (SHELL_CONFIG.debug) appwrapNativeLog(`[native:lifecycle] manageSubscriptionsSheet done err=${message ?? 'none'}
|
|
222
|
-
// The
|
|
223
|
-
//
|
|
224
|
-
//
|
|
225
|
-
|
|
226
|
-
// floats above our window and swallows touches — together they clear the post-sheet freeze.)
|
|
227
|
-
bridge.getWebView()?.wakeWebContent();
|
|
219
|
+
AppwrapManageSubscriptions.present((message: string | null) => {
|
|
220
|
+
if (SHELL_CONFIG.debug) appwrapNativeLog(`[native:lifecycle] manageSubscriptionsSheet done err=${message ?? 'none'}`);
|
|
221
|
+
// The sheet dismisses WITHOUT the app scene leaving foregroundActive → no resumeEvent, and it
|
|
222
|
+
// orphans a touch-stealing window above ours. Recover from this completion (the only callback
|
|
223
|
+
// that fires). Shared across all native surfaces; see CustomWebView.recoverAfterNativeSurface.
|
|
224
|
+
bridge.getWebView()?.recoverAfterNativeSurface();
|
|
228
225
|
if (message) reject(err('NATIVE_ERROR', message));
|
|
229
226
|
else resolve();
|
|
230
227
|
});
|
|
@@ -57,6 +57,8 @@ class PhotoPickerDelegate extends NSObject implements PHPickerViewControllerDele
|
|
|
57
57
|
}
|
|
58
58
|
pickerDidFinishPicking(picker: PHPickerViewController, results: NSArray<PHPickerResult>): void {
|
|
59
59
|
picker.dismissViewControllerAnimatedCompletion(true, null);
|
|
60
|
+
// Recover the WebView after the picker dismisses (orphaned-window / renderer-throttle guard).
|
|
61
|
+
bridge.getWebView()?.recoverAfterNativeSurface();
|
|
60
62
|
const result = results.count > 0 ? results.objectAtIndex(0) : null;
|
|
61
63
|
if (!result) return this.onResult?.({ picked: false });
|
|
62
64
|
result.itemProvider.loadObjectOfClassCompletionHandler(UIImage.class(), (img: UIImage) => {
|
|
@@ -51,6 +51,9 @@ export function registerOAuthHandlers(): void {
|
|
|
51
51
|
(callbackURL: NSURL, error: NSError) => {
|
|
52
52
|
activeSession = null;
|
|
53
53
|
activeProvider = null;
|
|
54
|
+
// ASWebAuthenticationSession can leave the WebView frozen on dismiss the same way the
|
|
55
|
+
// StoreKit sheet does (orphaned tracking window / no resume) — recover from its completion.
|
|
56
|
+
bridge.getWebView()?.recoverAfterNativeSurface();
|
|
54
57
|
if (error) {
|
|
55
58
|
// ASWebAuthenticationSessionErrorCodeCanceledLogin === 1 (user dismissed the sheet).
|
|
56
59
|
const code = error.code === 1 ? 'CANCELLED' : 'NATIVE_ERROR';
|
|
@@ -5,6 +5,20 @@ declare const androidx: any; // no NS types: androidx
|
|
|
5
5
|
|
|
6
6
|
const err = (code: string, message: string) => Object.assign(new Error(message), { code });
|
|
7
7
|
|
|
8
|
+
// SFSafariViewController is a same-app presented surface — on "Done" it can leave the WebView frozen
|
|
9
|
+
// (orphaned tracking window / throttled renderer), so recover from its didFinish. Strong-ref the
|
|
10
|
+
// delegate while the browser is up (ARC would otherwise free it). Mirrors the OAuth delegate pattern.
|
|
11
|
+
let activeSafariDelegate: NSObject | null = null;
|
|
12
|
+
@NativeClass()
|
|
13
|
+
class SafariDelegate extends NSObject implements SFSafariViewControllerDelegate {
|
|
14
|
+
static ObjCProtocols = [SFSafariViewControllerDelegate];
|
|
15
|
+
static new(): SafariDelegate { return <SafariDelegate>super.new(); }
|
|
16
|
+
safariViewControllerDidFinish(_controller: SFSafariViewController): void {
|
|
17
|
+
activeSafariDelegate = null;
|
|
18
|
+
bridge.getWebView()?.recoverAfterNativeSurface();
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
8
22
|
/** Running on a simulator/emulator? (mirrors env.ts detection; kept local to avoid a cross-import). */
|
|
9
23
|
function detectEmulator(): boolean {
|
|
10
24
|
if (isIOS) {
|
|
@@ -190,6 +204,8 @@ export function registerSystemHandlers(): void {
|
|
|
190
204
|
Utils.dispatchToMainThread(() => {
|
|
191
205
|
const vc = SFSafariViewController.alloc().initWithURL(NSURL.URLWithString(target));
|
|
192
206
|
if (toolbarColor) vc.preferredControlTintColor = new Color(String(toolbarColor)).ios;
|
|
207
|
+
activeSafariDelegate = SafariDelegate.new();
|
|
208
|
+
vc.delegate = activeSafariDelegate as SFSafariViewControllerDelegate;
|
|
193
209
|
Utils.ios.getRootViewController().presentViewControllerAnimatedCompletion(vc, true, null);
|
|
194
210
|
});
|
|
195
211
|
} else if (isAndroid) {
|
|
@@ -88,6 +88,9 @@ export function registerHandlers(): void {
|
|
|
88
88
|
if (controller.popoverPresentationController) {
|
|
89
89
|
controller.popoverPresentationController.sourceView = rootVC.view;
|
|
90
90
|
}
|
|
91
|
+
// Recover the WebView on dismiss — a presented sheet can orphan a touch-stealing window / leave
|
|
92
|
+
// the renderer throttled (see CustomWebView.recoverAfterNativeSurface).
|
|
93
|
+
controller.completionWithItemsHandler = () => { bridge.getWebView()?.recoverAfterNativeSurface(); };
|
|
91
94
|
rootVC.presentViewControllerAnimatedCompletion(controller, true, null);
|
|
92
95
|
} else if (isAndroid) {
|
|
93
96
|
const intent = new android.content.Intent(android.content.Intent.ACTION_SEND);
|
package/runtime/modules-native/billing/App_Resources/iOS/src/AppwrapManageSubscriptions.swift
CHANGED
|
@@ -5,87 +5,36 @@ import UIKit
|
|
|
5
5
|
// StoreKit 2's `AppStore.showManageSubscriptions(in:)` is Swift-only + async, so it can't be reached
|
|
6
6
|
// from the ObjC bridge NativeScript uses. This @objc wrapper bridges the async call to a completion
|
|
7
7
|
// callback (NativeScript can't await Swift async) and runs it on the main actor against the active
|
|
8
|
-
// foreground UIWindowScene. Shipped via the billing module's nativeSrc → compiled
|
|
9
|
-
//
|
|
8
|
+
// foreground UIWindowScene. Shipped via the billing module's nativeSrc → compiled only when `billing`
|
|
9
|
+
// is active. iOS 15+ (StoreKit 2); the JS layer guards the deep-link fallback.
|
|
10
10
|
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
// the app underneath stays alive but unresponsive ("frozen for minutes" until iOS reaps the window).
|
|
16
|
-
// We recover explicitly from THIS completion (the only thing that fires): after letting the dismissal
|
|
17
|
-
// settle, neutralise any stray above-our-main window (uie=false + hide) and re-key our window. The JS
|
|
18
|
-
// layer additionally drives the WebView's own resume (wakeWebContent) since no native resume arrives.
|
|
11
|
+
// NOTE: this sheet presents/dismisses WITHOUT the app scene leaving foregroundActive, so iOS fires no
|
|
12
|
+
// resumeEvent and leaves an orphaned interactive tracking-window above ours that swallows touches. The
|
|
13
|
+
// JS handler recovers from THIS completion via `CustomWebView.recoverAfterNativeSurface()` (shared with
|
|
14
|
+
// every other native surface) — so this shim only needs to present + report success/failure.
|
|
19
15
|
@objc(AppwrapManageSubscriptions)
|
|
20
16
|
public final class AppwrapManageSubscriptions: NSObject {
|
|
21
|
-
// completion(
|
|
22
|
-
//
|
|
23
|
-
@objc public static func present(_ completion: @escaping (String
|
|
17
|
+
// completion(nil) on success; completion(message) on failure. Always invoked on the main thread so
|
|
18
|
+
// the NS JS continuation (which then drives the WebView recovery) resumes on the UI thread.
|
|
19
|
+
@objc public static func present(_ completion: @escaping (String?) -> Void) {
|
|
24
20
|
guard #available(iOS 15.0, *) else {
|
|
25
|
-
completion("StoreKit 2 manage-subscriptions sheet requires iOS 15+"
|
|
21
|
+
completion("StoreKit 2 manage-subscriptions sheet requires iOS 15+")
|
|
26
22
|
return
|
|
27
23
|
}
|
|
28
24
|
Task { @MainActor in
|
|
29
25
|
guard let scene = AppwrapManageSubscriptions.activeWindowScene() else {
|
|
30
|
-
completion("No active UIWindowScene to present the subscriptions sheet"
|
|
26
|
+
completion("No active UIWindowScene to present the subscriptions sheet")
|
|
31
27
|
return
|
|
32
28
|
}
|
|
33
|
-
var err: String? = nil
|
|
34
29
|
do {
|
|
35
30
|
try await AppStore.showManageSubscriptions(in: scene)
|
|
31
|
+
completion(nil)
|
|
36
32
|
} catch {
|
|
37
|
-
|
|
33
|
+
completion(error.localizedDescription)
|
|
38
34
|
}
|
|
39
|
-
let before = AppwrapManageSubscriptions.dump(scene, tag: "BEFORE")
|
|
40
|
-
// The await can return before the dismissal animation finishes finalizing the orphan tracking
|
|
41
|
-
// window — settle first, then neutralise it (hiding at completion-time was too early to stick).
|
|
42
|
-
try? await Task.sleep(nanoseconds: 350_000_000)
|
|
43
|
-
let after = AppwrapManageSubscriptions.recover(scene)
|
|
44
|
-
completion(err, before + "\n" + after)
|
|
45
35
|
}
|
|
46
36
|
}
|
|
47
37
|
|
|
48
|
-
// Read-only dump of the scene's windows (+ presented-VC chain, key/interaction flags) for diagnosis.
|
|
49
|
-
@available(iOS 15.0, *)
|
|
50
|
-
@MainActor private static func dump(_ scene: UIWindowScene, tag: String) -> String {
|
|
51
|
-
var lines: [String] = ["\(tag): windows=\(scene.windows.count) keyWin=\(scene.keyWindow.map { ptr($0) } ?? "nil")"]
|
|
52
|
-
for (i, w) in scene.windows.enumerated() {
|
|
53
|
-
var presented: [String] = []
|
|
54
|
-
var vc = w.rootViewController
|
|
55
|
-
while let p = vc?.presentedViewController { presented.append(String(describing: type(of: p))); vc = p }
|
|
56
|
-
lines.append(" [\(i)] \(ptr(w)) level=\(w.windowLevel.rawValue) key=\(w.isKeyWindow) hidden=\(w.isHidden) uie=\(w.isUserInteractionEnabled) root=\(w.rootViewController.map { String(describing: type(of: $0)) } ?? "nil") presented=\(presented.isEmpty ? "none" : presented.joined(separator: ">"))")
|
|
57
|
-
}
|
|
58
|
-
return lines.joined(separator: "\n")
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// Neutralise any window sitting ABOVE our main app window (touch-stealer), dismiss a lingering
|
|
62
|
-
// presented controller, and re-assert our main window as key. Returns the post-recovery dump.
|
|
63
|
-
@available(iOS 15.0, *)
|
|
64
|
-
@MainActor private static func recover(_ scene: UIWindowScene) -> String {
|
|
65
|
-
var lines: [String] = []
|
|
66
|
-
let main = scene.windows
|
|
67
|
-
.filter { !$0.isHidden && $0.rootViewController != nil }
|
|
68
|
-
.min { $0.windowLevel.rawValue < $1.windowLevel.rawValue }
|
|
69
|
-
for (i, w) in scene.windows.enumerated() {
|
|
70
|
-
if let presented = w.rootViewController?.presentedViewController {
|
|
71
|
-
presented.dismiss(animated: false)
|
|
72
|
-
lines.append(" recover: dismissed \(String(describing: type(of: presented))) on win[\(i)]")
|
|
73
|
-
}
|
|
74
|
-
if let m = main, w !== m, w.windowLevel.rawValue >= m.windowLevel.rawValue, w.windowLevel.rawValue > 0 {
|
|
75
|
-
// Pass touches THROUGH to our app window below, and take it out of the hierarchy.
|
|
76
|
-
w.isUserInteractionEnabled = false
|
|
77
|
-
w.isHidden = true
|
|
78
|
-
lines.append(" recover: neutralised win[\(i)] level=\(w.windowLevel.rawValue) root=\(w.rootViewController.map { String(describing: type(of: $0)) } ?? "nil")")
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
if let m = main { m.makeKeyAndVisible(); lines.append(" recover: re-keyed main") }
|
|
82
|
-
return AppwrapManageSubscriptions.dump(scene, tag: "AFTER-RECOVER") + "\n" + lines.joined(separator: "\n")
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
@MainActor private static func ptr(_ w: UIWindow) -> String {
|
|
86
|
-
return "UIWindow<\(UInt(bitPattern: ObjectIdentifier(w).hashValue) & 0xffffff)>"
|
|
87
|
-
}
|
|
88
|
-
|
|
89
38
|
// The foreground-active window scene (the one allowed to present UI). Falls back to any connected
|
|
90
39
|
// window scene so a not-yet-foreground launch still finds a presenter.
|
|
91
40
|
@MainActor private static func activeWindowScene() -> UIWindowScene? {
|