@livx.cc/appwrap 0.23.2 → 0.25.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 +1 -1
- package/runtime/App_Resources/Android/src/main/res/xml/file_paths.xml +7 -1
- package/runtime/app/main-page.ts +4 -0
- package/runtime/app/shell/capabilities.manifest.ts +64 -12
- package/runtime/app/shell/config.ts +9 -0
- package/runtime/app/shell/custom-webview.android.ts +2 -2
- package/runtime/app/shell/custom-webview.ios.ts +3 -2
- package/runtime/app/shell/events.ts +11 -0
- package/runtime/app/shell/handlers-android.ts +43 -0
- package/runtime/app/shell/handlers-extended.ts +4 -1
- package/runtime/app/shell/handlers-fs.ts +372 -0
- package/runtime/app/shell/handlers-oauth.ts +90 -1
- package/runtime/app/shell/handlers-parity.ts +41 -13
- package/runtime/app/shell/handlers-push.ts +24 -1
- package/runtime/app/shell/handlers-reviews.ts +73 -0
- package/runtime/app/shell/handlers-scanner.ts +222 -0
- package/runtime/app/shell/handlers-speech.ts +336 -0
- package/runtime/app/shell/ios-frameworks.d.ts +8 -0
- package/runtime/app/shell/keyboard.ts +85 -0
- package/runtime/app/shell/web-quirks.ts +58 -0
- package/runtime/package.json +1 -1
- package/src/cli.ts +29 -2
- package/src/config.ts +11 -0
|
@@ -119,6 +119,64 @@ export function serviceWorkerGuardJs(enabled: boolean): string {
|
|
|
119
119
|
})();`;
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
+
/**
|
|
123
|
+
* Make the shell behave like a regular native app when the web content tries to leave the app's own
|
|
124
|
+
* origin: hand external-origin navigations to the OS default browser (Safari / Chrome — via the core
|
|
125
|
+
* `app.openUrl` handler, which leaves the app) instead of replacing the shell WebView. Intercepts, at
|
|
126
|
+
* document-start (BEFORE the page wires its own handlers), two intents:
|
|
127
|
+
* - a click on an `<a href>` resolving to a DIFFERENT origin (incl. `target="_blank"`), and
|
|
128
|
+
* - `window.open(url, ...)` to a different origin.
|
|
129
|
+
* Same-origin links / SPA route changes pass straight through (the app keeps owning its own
|
|
130
|
+
* navigation), so this never ejects the user mid-flow inside their own app. Only runs in the top
|
|
131
|
+
* frame — embedded iframes (maps, video, ad frames) are left alone. Non-http(s) schemes
|
|
132
|
+
* (`tel:`/`mailto:`/`app:`) are ignored here and handled by the OS/WebView as usual.
|
|
133
|
+
*
|
|
134
|
+
* The request is dispatched fire-and-forget over the same raw transport the bridge listens on (iOS
|
|
135
|
+
* `webkit.messageHandlers.appwrap`, Android `appwrapNative.postMessage`) — independent of the
|
|
136
|
+
* consumer's native-kit, so it works even on an unmodified remotely-loaded (loader:'server') webapp.
|
|
137
|
+
*
|
|
138
|
+
* `enabled` = SHELL_CONFIG.openNewWindowsInBrowser (default false). Idempotent via a window guard.
|
|
139
|
+
*/
|
|
140
|
+
export function externalNavGuardJs(enabled: boolean): string {
|
|
141
|
+
if (!enabled) return '';
|
|
142
|
+
return `(function(){
|
|
143
|
+
if (window.top !== window.self || window.__appwrapExtNavGuard) return;
|
|
144
|
+
window.__appwrapExtNavGuard = true;
|
|
145
|
+
function send(url){
|
|
146
|
+
try {
|
|
147
|
+
var json = JSON.stringify({ v:1, id:'extnav-'+Date.now()+'-'+Math.random().toString(36).slice(2),
|
|
148
|
+
kind:'request', method:'app.openUrl', params:{ url:url } });
|
|
149
|
+
if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.appwrap) {
|
|
150
|
+
window.webkit.messageHandlers.appwrap.postMessage(json); return true; }
|
|
151
|
+
if (window.appwrapNative && window.appwrapNative.postMessage) {
|
|
152
|
+
window.appwrapNative.postMessage(json); return true; }
|
|
153
|
+
} catch (e) {}
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
// Resolve to an absolute http(s) URL that is on a DIFFERENT origin than the app shell; else null.
|
|
157
|
+
function external(href){
|
|
158
|
+
try {
|
|
159
|
+
var u = new URL(href, location.href);
|
|
160
|
+
if (u.protocol !== 'http:' && u.protocol !== 'https:') return null; // tel:/mailto:/app:/blob: → OS/WebView
|
|
161
|
+
return u.origin !== location.origin ? u.href : null;
|
|
162
|
+
} catch (e) { return null; }
|
|
163
|
+
}
|
|
164
|
+
document.addEventListener('click', function(e){
|
|
165
|
+
if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
|
|
166
|
+
var a = e.target && e.target.closest ? e.target.closest('a[href]') : null;
|
|
167
|
+
if (!a) return;
|
|
168
|
+
var url = external(a.getAttribute('href'));
|
|
169
|
+
if (url && send(url)) { e.preventDefault(); e.stopPropagation(); }
|
|
170
|
+
}, true);
|
|
171
|
+
var open = window.open;
|
|
172
|
+
window.open = function(url){
|
|
173
|
+
var ext = url != null ? external(String(url)) : null;
|
|
174
|
+
if (ext && send(ext)) return null;
|
|
175
|
+
return open ? open.apply(window, arguments) : null;
|
|
176
|
+
};
|
|
177
|
+
})();`;
|
|
178
|
+
}
|
|
179
|
+
|
|
122
180
|
/**
|
|
123
181
|
* Native-feel injection shared by both CustomWebViews. Suppresses the "it's a
|
|
124
182
|
* web page" tells: pinch / double-tap zoom, long-press callout & selection,
|
package/runtime/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -172,6 +172,9 @@ function nativeReqs(cfg: AppwrapConfig): NativeReqs {
|
|
|
172
172
|
const OPTIONAL_GROUP_HANDLERS: Record<string, { file: string; fn: string }> = {
|
|
173
173
|
health: { file: './handlers-health', fn: 'registerHealthHandlers' },
|
|
174
174
|
oauth: { file: './handlers-oauth', fn: 'registerOAuthHandlers' },
|
|
175
|
+
reviews: { file: './handlers-reviews', fn: 'registerReviewsHandlers' },
|
|
176
|
+
scanner: { file: './handlers-scanner', fn: 'registerScannerHandlers' },
|
|
177
|
+
speech: { file: './handlers-speech', fn: 'registerSpeechHandlers' },
|
|
175
178
|
};
|
|
176
179
|
|
|
177
180
|
/** Generate the two composition artifacts in the wrapper: the active capability list (drives the
|
|
@@ -363,8 +366,10 @@ export const SHELL_CONFIG = {
|
|
|
363
366
|
debugLog: ${JSON.stringify(cfg.debugLog ?? '*')},
|
|
364
367
|
devMenu: ${JSON.stringify(cfg.devMenu ?? true)},
|
|
365
368
|
neutralizeServiceWorker: ${JSON.stringify(cfg.neutralizeServiceWorker ?? true)},
|
|
369
|
+
openNewWindowsInBrowser: ${JSON.stringify(cfg.openNewWindowsInBrowser ?? false)},
|
|
366
370
|
pushIos: ${JSON.stringify(!!cfg.push?.enabled && cfg.push?.ios !== false)},
|
|
367
371
|
pushAndroid: ${JSON.stringify(!!cfg.push?.enabled && cfg.push?.android !== false)},
|
|
372
|
+
pushRegistrationUrl: ${JSON.stringify(cfg.push?.registrationUrl ?? '')},
|
|
368
373
|
};
|
|
369
374
|
`;
|
|
370
375
|
writeFileSync(join(outDir, 'app/shell/config.ts'), content);
|
|
@@ -433,7 +438,15 @@ function stampIOSDisplayName(outDir: string, cfg: AppwrapConfig, req: NativeReqs
|
|
|
433
438
|
|
|
434
439
|
function stampTeamId(outDir: string, cfg: AppwrapConfig): void {
|
|
435
440
|
const xcconfig = join(outDir, 'App_Resources/iOS/build.xcconfig');
|
|
436
|
-
|
|
441
|
+
// A placeholder/empty teamId silently produces an unsignable build (DEVELOPMENT_TEAM = YOUR_APPLE_TEAM_ID
|
|
442
|
+
// → xcodebuild "No Account for Team"). Warn loudly rather than let a long build fail cryptically.
|
|
443
|
+
if (!cfg.teamId || /YOUR_APPLE_TEAM_ID|^$/.test(cfg.teamId)) {
|
|
444
|
+
console.warn(`⚠ teamId is unset/placeholder ("${cfg.teamId ?? ''}") — device builds won't sign.\n` +
|
|
445
|
+
' Set it to your Apple Team ID in appwrap.config (Xcode → Settings → Accounts shows it; ' +
|
|
446
|
+
'Individual = paid, Personal Team = free).');
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
if (!existsSync(xcconfig)) return;
|
|
437
450
|
let src = readFileSync(xcconfig, 'utf8');
|
|
438
451
|
src = /DEVELOPMENT_TEAM\s*=/.test(src)
|
|
439
452
|
? src.replace(/DEVELOPMENT_TEAM\s*=\s*[^;\n]*;?/, `DEVELOPMENT_TEAM = ${cfg.teamId};`)
|
|
@@ -1146,7 +1159,21 @@ async function deploy(cwd: string, flags: Record<string, string>, positionals: s
|
|
|
1146
1159
|
// Dev deploy → debug mode: keep-awake + WebView inspector for continuous troubleshooting.
|
|
1147
1160
|
stampShellConfig(outDir, { ...cfg, debug: true });
|
|
1148
1161
|
console.log('▶ ns build ios --for-device (debug: keep-awake + inspector on)');
|
|
1149
|
-
|
|
1162
|
+
try {
|
|
1163
|
+
execFileSync('ns', ['build', 'ios', '--for-device'], { cwd: outDir, stdio: 'inherit' });
|
|
1164
|
+
} catch (e) {
|
|
1165
|
+
// The xcodebuild dump above is cryptic; surface the two signing failures we actually hit most.
|
|
1166
|
+
console.error(
|
|
1167
|
+
'\n✖ Device build failed — if the errors above mention signing:\n' +
|
|
1168
|
+
` • "Failed Registering Bundle Identifier … not available" → the App ID "${cfg.id}" is already\n` +
|
|
1169
|
+
' registered to another team (e.g. a prior free-team build). Change `id` in appwrap.config to a\n' +
|
|
1170
|
+
' unique string and re-deploy.\n' +
|
|
1171
|
+
' • "profile doesn\'t include the … entitlement" (e.g. HealthKit) → that capability needs a PAID\n' +
|
|
1172
|
+
' team (Individual). A free Personal Team can\'t hold it — switch teamId or drop the module.\n' +
|
|
1173
|
+
' • "No Account for Team" → sign that Apple ID into Xcode → Settings → Accounts first.'
|
|
1174
|
+
);
|
|
1175
|
+
process.exit(1);
|
|
1176
|
+
}
|
|
1150
1177
|
|
|
1151
1178
|
const ipaDir = join(outDir, 'platforms/ios/build/Debug-iphoneos');
|
|
1152
1179
|
const ipa = existsSync(ipaDir) ? readdirSync(ipaDir).find((f) => f.endsWith('.ipa')) : undefined;
|
package/src/config.ts
CHANGED
|
@@ -79,6 +79,13 @@ export interface AppwrapConfig {
|
|
|
79
79
|
* for in-WebView web-push as a fallback. NOTE: keeping the SW is the only way to get web push; native
|
|
80
80
|
* push is the separate `push` lane (APNs/FCM) and does NOT need a SW. */
|
|
81
81
|
neutralizeServiceWorker?: boolean;
|
|
82
|
+
/** Hand navigations that LEAVE the app's own origin to the OS default browser (Safari / Chrome)
|
|
83
|
+
* instead of replacing the shell WebView — so external links and `window.open(...)`/`target="_blank"`
|
|
84
|
+
* behave like a regular native app (open in the system browser) rather than navigating away inside
|
|
85
|
+
* the shell. Covers external-origin `<a>` clicks (including `target="_blank"`) and `window.open()`;
|
|
86
|
+
* same-origin SPA navigation, subframes/iframes and `tel:`/`mailto:` are left untouched. Default
|
|
87
|
+
* `false` (unchanged in-WebView navigation). See `externalNavGuardJs`. */
|
|
88
|
+
openNewWindowsInBrowser?: boolean;
|
|
82
89
|
/** Apple Development Team ID for device builds (Xcode → Settings → Accounts). */
|
|
83
90
|
teamId?: string;
|
|
84
91
|
/** Path (relative to the PWA project) to a StoreKit configuration file for LOCAL IAP
|
|
@@ -132,6 +139,10 @@ export interface AppwrapConfig {
|
|
|
132
139
|
apsEnvironment?: 'development' | 'production';
|
|
133
140
|
/** Path (relative to the PWA project) to the Firebase `google-services.json` for Android FCM. */
|
|
134
141
|
googleServicesJson?: string;
|
|
142
|
+
/** Optional backend URL the shell POSTs the device token to NATIVELY on acquisition (`{token,
|
|
143
|
+
* platform}`). Native HTTP sidesteps the WKWebView app:// cross-origin wall — the token reaches
|
|
144
|
+
* your server with no WebView fetch. Your backend stores it + sends pushes (provider-agnostic). */
|
|
145
|
+
registrationUrl?: string;
|
|
135
146
|
};
|
|
136
147
|
}
|
|
137
148
|
|