@glassly/engine 0.3.7-beta.889 → 0.3.7-beta.896
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/build/engine.d.ts +6 -0
- package/build/engine.d.ts.map +1 -1
- package/build/engine.js +3 -0
- package/build/engine.js.map +1 -1
- package/build/facades/backup.d.ts +14 -0
- package/build/facades/backup.d.ts.map +1 -0
- package/build/facades/backup.js +22 -0
- package/build/facades/backup.js.map +1 -0
- package/build/generated/releaseMetadata.js +3 -3
- package/build/generated/releaseMetadata.js.map +1 -1
- package/build/index.d.ts +1 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js.map +1 -1
- package/build/services/AppRegistry.d.ts +2 -1
- package/build/services/AppRegistry.d.ts.map +1 -1
- package/build/services/AppRegistry.js +9 -2
- package/build/services/AppRegistry.js.map +1 -1
- package/build/services/AppStateBackup.d.ts +98 -0
- package/build/services/AppStateBackup.d.ts.map +1 -0
- package/build/services/AppStateBackup.js +224 -0
- package/build/services/AppStateBackup.js.map +1 -0
- package/build/services/CloudClientService.d.ts +1 -0
- package/build/services/CloudClientService.d.ts.map +1 -1
- package/build/services/LocalMiniappRuntime.d.ts.map +1 -1
- package/build/services/LocalMiniappRuntime.js +18 -4
- package/build/services/LocalMiniappRuntime.js.map +1 -1
- package/build/services/MiniappEngine.d.ts +9 -0
- package/build/services/MiniappEngine.d.ts.map +1 -1
- package/build/services/MiniappEngine.js +60 -27
- package/build/services/MiniappEngine.js.map +1 -1
- package/build/services/NavigationService.d.ts +1 -0
- package/build/services/NavigationService.d.ts.map +1 -1
- package/build/services/NavigationService.js +20 -6
- package/build/services/NavigationService.js.map +1 -1
- package/build/services/appStateBackupFile.d.ts +32 -0
- package/build/services/appStateBackupFile.d.ts.map +1 -0
- package/build/services/appStateBackupFile.js +135 -0
- package/build/services/appStateBackupFile.js.map +1 -0
- package/build/utils/miniappGlobals.d.ts +13 -17
- package/build/utils/miniappGlobals.d.ts.map +1 -1
- package/build/utils/miniappGlobals.js +24 -56
- package/build/utils/miniappGlobals.js.map +1 -1
- package/package.json +7 -7
- package/src/engine.ts +3 -0
- package/src/facades/backup.ts +28 -0
- package/src/generated/releaseMetadata.ts +3 -3
- package/src/index.ts +1 -0
- package/src/services/AppRegistry.ts +8 -3
- package/src/services/AppStateBackup.ts +283 -0
- package/src/services/LocalMiniappRuntime.ts +21 -4
- package/src/services/MiniappEngine.ts +62 -27
- package/src/services/NavigationService.ts +28 -8
- package/src/services/appStateBackupFile.ts +171 -0
- package/src/utils/miniappGlobals.ts +32 -64
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App state backup files: the share-sheet export and file-picker restore
|
|
3
|
+
* behind Settings > Profile > Backup. What goes in the file is decided in
|
|
4
|
+
* AppStateBackup.ts; this side reads and writes the live stores.
|
|
5
|
+
*/
|
|
6
|
+
import { File, Paths } from "expo-file-system";
|
|
7
|
+
import { Platform } from "react-native";
|
|
8
|
+
import Share from "react-native-share";
|
|
9
|
+
import { SETTINGS, useSettingsStore } from "../stores/settings";
|
|
10
|
+
import { useAppStatusStore } from "../stores/apps";
|
|
11
|
+
import { storage } from "../utils/storage/storage";
|
|
12
|
+
import appRegistry, { saveAlwaysOnOverride, saveWidgetExpandChoice } from "./AppRegistry";
|
|
13
|
+
import { AppStateBackupError, collectAppStateBackup, miniappPrefs, miniappsWithStorage, parseAppStateBackup, restorableSettings, restoreMiniappStorage, } from "./AppStateBackup";
|
|
14
|
+
import { cloudClientService } from "./CloudClientService";
|
|
15
|
+
import { memoryStore } from "./memoryStoreInstance";
|
|
16
|
+
import { respawnMiniapp, runningMiniappPackages, stopMiniapp } from "./MiniappEngine";
|
|
17
|
+
const PICK_MIME_TYPES = Platform.OS === "android" ? ["application/json", "application/octet-stream"] : ["application/json"];
|
|
18
|
+
const backend = {
|
|
19
|
+
get(key) {
|
|
20
|
+
const result = storage.load(key);
|
|
21
|
+
return result.is_ok() ? result.value : null;
|
|
22
|
+
},
|
|
23
|
+
set(key, value) {
|
|
24
|
+
const result = storage.save(key, value);
|
|
25
|
+
if (result.is_error())
|
|
26
|
+
throw result.error;
|
|
27
|
+
},
|
|
28
|
+
keys: () => storage.getAllKeys(),
|
|
29
|
+
};
|
|
30
|
+
export async function exportAppStateFile() {
|
|
31
|
+
const userId = await cloudClientService.resolveUserId();
|
|
32
|
+
const installed = [];
|
|
33
|
+
for (const app of await appRegistry.getInstalledMiniapps()) {
|
|
34
|
+
const version = await appRegistry.getActiveVersion(app.packageName);
|
|
35
|
+
if (!version)
|
|
36
|
+
continue;
|
|
37
|
+
installed.push({
|
|
38
|
+
packageName: app.packageName,
|
|
39
|
+
version,
|
|
40
|
+
source: appRegistry.getReleaseIdentity(app.packageName, version)?.source,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
const backup = collectAppStateBackup({
|
|
44
|
+
backend,
|
|
45
|
+
userId,
|
|
46
|
+
settings: Object.values(SETTINGS),
|
|
47
|
+
installed,
|
|
48
|
+
memory: await memoryStore.exportBackup(),
|
|
49
|
+
now: Date.now(),
|
|
50
|
+
});
|
|
51
|
+
const filename = `glassly-backup-${localDate(backup.exportedAt)}.json`;
|
|
52
|
+
const file = new File(Paths.cache, filename);
|
|
53
|
+
file.writeSync(JSON.stringify(backup));
|
|
54
|
+
try {
|
|
55
|
+
await Share.open({ url: file.uri, type: "application/json", filename, title: "Glassly backup" });
|
|
56
|
+
return { success: true };
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
// react-native-share rejects when the user dismisses the sheet.
|
|
60
|
+
if (error?.message?.includes("User did not share"))
|
|
61
|
+
return { success: false, cancelled: true };
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/** Resolves null when the user cancels the picker. */
|
|
66
|
+
export async function importAppStateFile(opts = {}) {
|
|
67
|
+
const picked = await File.pickFileAsync({ mimeTypes: PICK_MIME_TYPES });
|
|
68
|
+
if (picked.canceled)
|
|
69
|
+
return null;
|
|
70
|
+
let data;
|
|
71
|
+
try {
|
|
72
|
+
data = JSON.parse(await picked.result.text());
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
throw new AppStateBackupError("Not a Glassly app state backup");
|
|
76
|
+
}
|
|
77
|
+
const backup = parseAppStateBackup(data);
|
|
78
|
+
const userId = await cloudClientService.resolveUserId();
|
|
79
|
+
// First, so a memory section this build rejects fails the restore before anything is written.
|
|
80
|
+
const memory = backup.memory ? await memoryStore.importBackup(backup.memory) : null;
|
|
81
|
+
const { plain, indexed } = restorableSettings(backup, Object.values(SETTINGS));
|
|
82
|
+
const written = await useSettingsStore.getState().setManyLocally(plain);
|
|
83
|
+
if (written.is_error())
|
|
84
|
+
throw written.error;
|
|
85
|
+
// setManyLocally only takes registry keys; `key:index` values go to storage and the store directly.
|
|
86
|
+
for (const [key, value] of Object.entries(indexed))
|
|
87
|
+
backend.set(key, value);
|
|
88
|
+
if (Object.keys(indexed).length > 0) {
|
|
89
|
+
useSettingsStore.setState((state) => ({ settings: { ...state.settings, ...indexed } }));
|
|
90
|
+
}
|
|
91
|
+
// A running miniapp holds its old values in memory and could write them back over the
|
|
92
|
+
// restore, so the ones getting storage stop now and start again at the end.
|
|
93
|
+
const running = new Set(runningMiniappPackages());
|
|
94
|
+
const restarting = miniappsWithStorage(backup).filter((packageName) => running.has(packageName));
|
|
95
|
+
for (const packageName of restarting)
|
|
96
|
+
await stopMiniapp(packageName);
|
|
97
|
+
try {
|
|
98
|
+
const restored = restoreMiniappStorage(backend, userId, backup);
|
|
99
|
+
const apps = useAppStatusStore.getState();
|
|
100
|
+
for (const [packageName, record] of Object.entries(backup.miniapps)) {
|
|
101
|
+
const prefs = miniappPrefs(record);
|
|
102
|
+
if (prefs.hidden !== undefined)
|
|
103
|
+
apps.setHiddenStatus(packageName, prefs.hidden);
|
|
104
|
+
if (prefs.alwaysOn !== undefined)
|
|
105
|
+
saveAlwaysOnOverride(packageName, prefs.alwaysOn);
|
|
106
|
+
for (const [widgetId, expand] of Object.entries(prefs.widgetExpand)) {
|
|
107
|
+
saveWidgetExpandChoice(packageName, widgetId, expand);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
appRegistry.markRefreshNeeded();
|
|
111
|
+
await apps.refresh();
|
|
112
|
+
const installedNow = new Set((await appRegistry.getInstalledMiniapps()).map((app) => app.packageName));
|
|
113
|
+
const missing = Object.entries(backup.miniapps)
|
|
114
|
+
.filter(([packageName, record]) => record.installed && !installedNow.has(packageName))
|
|
115
|
+
.map(([packageName]) => packageName);
|
|
116
|
+
if (missing.length > 0)
|
|
117
|
+
await opts.reinstall?.(missing);
|
|
118
|
+
return {
|
|
119
|
+
settings: Object.keys(plain).length + Object.keys(indexed).length,
|
|
120
|
+
miniapps: restored.packages.length,
|
|
121
|
+
memory,
|
|
122
|
+
missing,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
finally {
|
|
126
|
+
for (const packageName of restarting)
|
|
127
|
+
await respawnMiniapp(packageName);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function localDate(ms) {
|
|
131
|
+
const d = new Date(ms);
|
|
132
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
133
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
|
|
134
|
+
}
|
|
135
|
+
//# sourceMappingURL=appStateBackupFile.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"appStateBackupFile.js","sourceRoot":"","sources":["../../src/services/appStateBackupFile.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * App state backup files: the share-sheet export and file-picker restore\n * behind Settings > Profile > Backup. What goes in the file is decided in\n * AppStateBackup.ts; this side reads and writes the live stores.\n */\n\nimport type {MemoryImportResult} from \"@glassly/miniapp\"\nimport {File, Paths} from \"expo-file-system\"\nimport {Platform} from \"react-native\"\nimport Share from \"react-native-share\"\n\nimport {SETTINGS, useSettingsStore} from \"../stores/settings\"\nimport {useAppStatusStore} from \"../stores/apps\"\nimport {storage} from \"../utils/storage/storage\"\nimport appRegistry, {saveAlwaysOnOverride, saveWidgetExpandChoice} from \"./AppRegistry\"\nimport {\n AppStateBackupError,\n collectAppStateBackup,\n miniappPrefs,\n miniappsWithStorage,\n parseAppStateBackup,\n restorableSettings,\n restoreMiniappStorage,\n type AppStateBackupBackend,\n} from \"./AppStateBackup\"\nimport {cloudClientService} from \"./CloudClientService\"\nimport {memoryStore} from \"./memoryStoreInstance\"\nimport {respawnMiniapp, runningMiniappPackages, stopMiniapp} from \"./MiniappEngine\"\n\nexport interface AppStateExportResult {\n success: boolean\n cancelled?: boolean\n}\n\nexport interface AppStateRestoreResult {\n /** Settings written. */\n settings: number\n /** Miniapps whose storage was restored. */\n miniapps: number\n memory: MemoryImportResult | null\n /** Installed when the backup was made but not on this phone now. */\n missing: string[]\n}\n\nexport interface AppStateRestoreOptions {\n /**\n * Install what the backup had installed and this phone lacks. Runs before\n * the restored miniapps start again, so the dashboard comes back with every\n * widget provider its saved layout names: it drops unknown widgets from the\n * layout the next time it saves it.\n */\n reinstall?: (packageNames: string[]) => Promise<void>\n}\n\nconst PICK_MIME_TYPES =\n Platform.OS === \"android\" ? [\"application/json\", \"application/octet-stream\"] : [\"application/json\"]\n\nconst backend: AppStateBackupBackend = {\n get(key) {\n const result = storage.load<unknown>(key)\n return result.is_ok() ? result.value : null\n },\n set(key, value) {\n const result = storage.save(key, value)\n if (result.is_error()) throw result.error\n },\n keys: () => storage.getAllKeys(),\n}\n\nexport async function exportAppStateFile(): Promise<AppStateExportResult> {\n const userId = await cloudClientService.resolveUserId()\n const installed: Array<{packageName: string; version: string; source?: string}> = []\n for (const app of await appRegistry.getInstalledMiniapps()) {\n const version = await appRegistry.getActiveVersion(app.packageName)\n if (!version) continue\n installed.push({\n packageName: app.packageName,\n version,\n source: appRegistry.getReleaseIdentity(app.packageName, version)?.source,\n })\n }\n const backup = collectAppStateBackup({\n backend,\n userId,\n settings: Object.values(SETTINGS),\n installed,\n memory: await memoryStore.exportBackup(),\n now: Date.now(),\n })\n\n const filename = `glassly-backup-${localDate(backup.exportedAt)}.json`\n const file = new File(Paths.cache, filename)\n file.writeSync(JSON.stringify(backup))\n try {\n await Share.open({url: file.uri, type: \"application/json\", filename, title: \"Glassly backup\"})\n return {success: true}\n } catch (error: any) {\n // react-native-share rejects when the user dismisses the sheet.\n if (error?.message?.includes(\"User did not share\")) return {success: false, cancelled: true}\n throw error\n }\n}\n\n/** Resolves null when the user cancels the picker. */\nexport async function importAppStateFile(opts: AppStateRestoreOptions = {}): Promise<AppStateRestoreResult | null> {\n const picked = await File.pickFileAsync({mimeTypes: PICK_MIME_TYPES})\n if (picked.canceled) return null\n let data: unknown\n try {\n data = JSON.parse(await picked.result.text())\n } catch {\n throw new AppStateBackupError(\"Not a Glassly app state backup\")\n }\n const backup = parseAppStateBackup(data)\n const userId = await cloudClientService.resolveUserId()\n\n // First, so a memory section this build rejects fails the restore before anything is written.\n const memory = backup.memory ? await memoryStore.importBackup(backup.memory) : null\n\n const {plain, indexed} = restorableSettings(backup, Object.values(SETTINGS))\n const written = await useSettingsStore.getState().setManyLocally(plain)\n if (written.is_error()) throw written.error\n // setManyLocally only takes registry keys; `key:index` values go to storage and the store directly.\n for (const [key, value] of Object.entries(indexed)) backend.set(key, value)\n if (Object.keys(indexed).length > 0) {\n useSettingsStore.setState((state) => ({settings: {...state.settings, ...indexed}}))\n }\n\n // A running miniapp holds its old values in memory and could write them back over the\n // restore, so the ones getting storage stop now and start again at the end.\n const running = new Set(runningMiniappPackages())\n const restarting = miniappsWithStorage(backup).filter((packageName) => running.has(packageName))\n for (const packageName of restarting) await stopMiniapp(packageName)\n\n try {\n const restored = restoreMiniappStorage(backend, userId, backup)\n\n const apps = useAppStatusStore.getState()\n for (const [packageName, record] of Object.entries(backup.miniapps)) {\n const prefs = miniappPrefs(record)\n if (prefs.hidden !== undefined) apps.setHiddenStatus(packageName, prefs.hidden)\n if (prefs.alwaysOn !== undefined) saveAlwaysOnOverride(packageName, prefs.alwaysOn)\n for (const [widgetId, expand] of Object.entries(prefs.widgetExpand)) {\n saveWidgetExpandChoice(packageName, widgetId, expand)\n }\n }\n appRegistry.markRefreshNeeded()\n await apps.refresh()\n\n const installedNow = new Set((await appRegistry.getInstalledMiniapps()).map((app) => app.packageName))\n const missing = Object.entries(backup.miniapps)\n .filter(([packageName, record]) => record.installed && !installedNow.has(packageName))\n .map(([packageName]) => packageName)\n if (missing.length > 0) await opts.reinstall?.(missing)\n\n return {\n settings: Object.keys(plain).length + Object.keys(indexed).length,\n miniapps: restored.packages.length,\n memory,\n missing,\n }\n } finally {\n for (const packageName of restarting) await respawnMiniapp(packageName)\n }\n}\n\nfunction localDate(ms: number): string {\n const d = new Date(ms)\n const pad = (n: number) => String(n).padStart(2, \"0\")\n return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`\n}\n"]}
|
|
@@ -12,6 +12,11 @@ export interface MiniappSafeArea {
|
|
|
12
12
|
left: number;
|
|
13
13
|
right: number;
|
|
14
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* The host's capsule menu in the WebView's own coordinate space. `corner`
|
|
17
|
+
* floats over the page's top-right; `rail` sits in the iPhone Duo's status
|
|
18
|
+
* rail, inside the side safe-area inset and clear of the page content.
|
|
19
|
+
*/
|
|
15
20
|
export interface CapsuleMenuRect {
|
|
16
21
|
top: number;
|
|
17
22
|
right: number;
|
|
@@ -19,14 +24,8 @@ export interface CapsuleMenuRect {
|
|
|
19
24
|
left: number;
|
|
20
25
|
width: number;
|
|
21
26
|
height: number;
|
|
27
|
+
placement: "corner" | "rail";
|
|
22
28
|
}
|
|
23
|
-
/**
|
|
24
|
-
* @param topInsetOffset extra top offset to add when the WebView container does
|
|
25
|
-
* NOT already pad by the safe-area inset (i.e. WebView fills edge-to-edge under
|
|
26
|
-
* the status bar). Pass `insets.top` in that case, or 0 when the container
|
|
27
|
-
* already applies top padding.
|
|
28
|
-
*/
|
|
29
|
-
export declare function getCapsuleMenuRect(topInsetOffset?: number): CapsuleMenuRect;
|
|
30
29
|
export type MiniappColorScheme = "light" | "dark";
|
|
31
30
|
export interface BuildMiniappGlobalsOptions {
|
|
32
31
|
packageName?: string;
|
|
@@ -34,21 +33,18 @@ export interface BuildMiniappGlobalsOptions {
|
|
|
34
33
|
miniappLocal?: boolean;
|
|
35
34
|
miniappDeveloperMode?: boolean;
|
|
36
35
|
safeAreaInsets: MiniappSafeArea;
|
|
37
|
-
/**
|
|
38
|
-
|
|
39
|
-
* the status bar). In that case the capsule menu rect needs to shift down by
|
|
40
|
-
* the top inset so its coords match where it's actually drawn on screen.
|
|
41
|
-
*/
|
|
42
|
-
webviewFillsStatusBar?: boolean;
|
|
36
|
+
/** Where the host draws its capsule menu over this WebView. */
|
|
37
|
+
capsuleMenu: CapsuleMenuRect;
|
|
43
38
|
/** Current host color scheme. Miniapps may follow this to match the phone. */
|
|
44
39
|
colorScheme?: MiniappColorScheme;
|
|
45
40
|
}
|
|
46
41
|
/**
|
|
47
|
-
* JS to inject into a running miniapp when the host's safe area
|
|
48
|
-
* iPhone Duo folding
|
|
49
|
-
* CSS variables, then fires
|
|
42
|
+
* JS to inject into a running miniapp when the host's safe area or capsule
|
|
43
|
+
* menu moves (an iPhone Duo folding, or its rail's reserved regions growing):
|
|
44
|
+
* updates window.Glassly and the CSS variables, then fires
|
|
45
|
+
* `glassly:safe-area-change`.
|
|
50
46
|
*/
|
|
51
|
-
export declare function buildSafeAreaUpdateScript(insets: MiniappSafeArea): string;
|
|
47
|
+
export declare function buildSafeAreaUpdateScript(insets: MiniappSafeArea, capsuleMenu: CapsuleMenuRect): string;
|
|
52
48
|
/**
|
|
53
49
|
* Returns the JS string to inject into a miniapp WebView before its content
|
|
54
50
|
* loads. Sets window.Glassly with the standard fields miniapps read:
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"miniappGlobals.d.ts","sourceRoot":"","sources":["../../src/utils/miniappGlobals.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,MAAM,WAAW,eAAe;IAC9B,GAAG,EAAE,MAAM,CAAA;IACX,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;CACd;AAED,MAAM,WAAW,eAAe;IAC9B,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;
|
|
1
|
+
{"version":3,"file":"miniappGlobals.d.ts","sourceRoot":"","sources":["../../src/utils/miniappGlobals.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAIH,MAAM,WAAW,eAAe;IAC9B,GAAG,EAAE,MAAM,CAAA;IACX,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;CACd;AAED;;;;GAIG;AACH,MAAM,WAAW,eAAe;IAC9B,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,SAAS,EAAE,QAAQ,GAAG,MAAM,CAAA;CAC7B;AAED,MAAM,MAAM,kBAAkB,GAAG,OAAO,GAAG,MAAM,CAAA;AAEjD,MAAM,WAAW,0BAA0B;IACzC,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAA;IACvB,YAAY,CAAC,EAAE,OAAO,CAAA;IACtB,oBAAoB,CAAC,EAAE,OAAO,CAAA;IAC9B,cAAc,EAAE,eAAe,CAAA;IAC/B,+DAA+D;IAC/D,WAAW,EAAE,eAAe,CAAA;IAC5B,8EAA8E;IAC9E,WAAW,CAAC,EAAE,kBAAkB,CAAA;CACjC;AAwBD;;;;;GAKG;AACH,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,eAAe,EAAE,WAAW,EAAE,eAAe,GAAG,MAAM,CAcvG;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,0BAA0B,GAAG,MAAM,CAqJlF"}
|
|
@@ -6,55 +6,42 @@
|
|
|
6
6
|
* this util is the one place that builds the globals so every WebView gets the
|
|
7
7
|
* same API surface.
|
|
8
8
|
*/
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
* - CapsuleButton height ≈ h-7.5 (30px), width ≈ 73px
|
|
15
|
-
* - Positioned at right-2 (8px), top = theme.spacing.s2 (8px) below insets.top
|
|
16
|
-
*/
|
|
17
|
-
const CAPSULE_MENU_HEIGHT = 30;
|
|
18
|
-
const CAPSULE_MENU_WIDTH = 73;
|
|
19
|
-
const CAPSULE_MENU_PADDING = 8;
|
|
20
|
-
/**
|
|
21
|
-
* @param topInsetOffset extra top offset to add when the WebView container does
|
|
22
|
-
* NOT already pad by the safe-area inset (i.e. WebView fills edge-to-edge under
|
|
23
|
-
* the status bar). Pass `insets.top` in that case, or 0 when the container
|
|
24
|
-
* already applies top padding.
|
|
25
|
-
*/
|
|
26
|
-
export function getCapsuleMenuRect(topInsetOffset = 0) {
|
|
27
|
-
const screenWidth = Dimensions.get("window").width;
|
|
28
|
-
const top = topInsetOffset + CAPSULE_MENU_PADDING;
|
|
29
|
-
return {
|
|
30
|
-
top,
|
|
31
|
-
right: CAPSULE_MENU_PADDING,
|
|
32
|
-
bottom: top + CAPSULE_MENU_HEIGHT,
|
|
33
|
-
left: screenWidth - CAPSULE_MENU_PADDING - CAPSULE_MENU_WIDTH,
|
|
34
|
-
width: CAPSULE_MENU_WIDTH,
|
|
35
|
-
height: CAPSULE_MENU_HEIGHT,
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
function safeAreaCssVars(insets) {
|
|
9
|
+
import { Platform } from "react-native";
|
|
10
|
+
// CSS custom properties that mirror the safe-area and capsule menu data, so
|
|
11
|
+
// miniapp CSS (or Tailwind arbitrary values) can read them without touching
|
|
12
|
+
// JS: e.g. `style="margin-top: var(--glassly-capsule-top)"`.
|
|
13
|
+
function layoutCssVars(insets, capsule) {
|
|
39
14
|
return {
|
|
40
15
|
"--glassly-safe-top": `${insets.top}px`,
|
|
41
16
|
"--glassly-safe-bottom": `${insets.bottom}px`,
|
|
42
17
|
"--glassly-safe-left": `${insets.left}px`,
|
|
43
18
|
"--glassly-safe-right": `${insets.right}px`,
|
|
19
|
+
"--glassly-capsule-top": `${capsule.top}px`,
|
|
20
|
+
"--glassly-capsule-bottom": `${capsule.bottom}px`,
|
|
21
|
+
"--glassly-capsule-left": `${capsule.left}px`,
|
|
22
|
+
"--glassly-capsule-right": `${capsule.right}px`,
|
|
23
|
+
"--glassly-capsule-width": `${capsule.width}px`,
|
|
24
|
+
"--glassly-capsule-height": `${capsule.height}px`,
|
|
25
|
+
"--glassly-capsule-center-y": `${capsule.top + capsule.height / 2}px`,
|
|
26
|
+
// Right-side gutter to reserve so content doesn't slide under the
|
|
27
|
+
// capsule: its width + 16px breathing room, none when it sits in the rail.
|
|
28
|
+
"--glassly-capsule-gutter": `${capsule.placement === "rail" ? 0 : capsule.width + 16}px`,
|
|
44
29
|
};
|
|
45
30
|
}
|
|
46
31
|
/**
|
|
47
|
-
* JS to inject into a running miniapp when the host's safe area
|
|
48
|
-
* iPhone Duo folding
|
|
49
|
-
* CSS variables, then fires
|
|
32
|
+
* JS to inject into a running miniapp when the host's safe area or capsule
|
|
33
|
+
* menu moves (an iPhone Duo folding, or its rail's reserved regions growing):
|
|
34
|
+
* updates window.Glassly and the CSS variables, then fires
|
|
35
|
+
* `glassly:safe-area-change`.
|
|
50
36
|
*/
|
|
51
|
-
export function buildSafeAreaUpdateScript(insets) {
|
|
52
|
-
const sets = Object.entries(
|
|
37
|
+
export function buildSafeAreaUpdateScript(insets, capsuleMenu) {
|
|
38
|
+
const sets = Object.entries(layoutCssVars(insets, capsuleMenu))
|
|
53
39
|
.map(([k, v]) => `document.documentElement.style.setProperty(${JSON.stringify(k)}, ${JSON.stringify(v)});`)
|
|
54
40
|
.join("\n ");
|
|
55
41
|
return `
|
|
56
42
|
window.Glassly = window.Glassly || {};
|
|
57
43
|
window.Glassly.safeAreaInsets = ${JSON.stringify(insets)};
|
|
44
|
+
window.Glassly.capsuleMenu = ${JSON.stringify(capsuleMenu)};
|
|
58
45
|
try {
|
|
59
46
|
${sets}
|
|
60
47
|
} catch (e) { /* ignore */ }
|
|
@@ -75,12 +62,11 @@ export function buildSafeAreaUpdateScript(insets) {
|
|
|
75
62
|
* assign to it before the native bridge wires in.
|
|
76
63
|
*/
|
|
77
64
|
export function buildMiniappGlobalsScript(opts) {
|
|
78
|
-
const capsuleTopOffset = opts.webviewFillsStatusBar ? opts.safeAreaInsets.top : 0;
|
|
79
65
|
const globals = {
|
|
80
66
|
platform: Platform.OS,
|
|
81
67
|
capabilities: opts.capabilities ?? ["share", "open_url", "copy_clipboard", "download"],
|
|
82
68
|
safeAreaInsets: opts.safeAreaInsets,
|
|
83
|
-
capsuleMenu:
|
|
69
|
+
capsuleMenu: opts.capsuleMenu,
|
|
84
70
|
};
|
|
85
71
|
if (opts.packageName)
|
|
86
72
|
globals.packageName = opts.packageName;
|
|
@@ -90,25 +76,7 @@ export function buildMiniappGlobalsScript(opts) {
|
|
|
90
76
|
globals.miniappDeveloperMode = true;
|
|
91
77
|
if (opts.colorScheme)
|
|
92
78
|
globals.colorScheme = opts.colorScheme;
|
|
93
|
-
|
|
94
|
-
// Miniapp CSS (or Tailwind arbitrary values) can read these without
|
|
95
|
-
// touching JS: e.g. `style="margin-top: var(--glassly-capsule-top)"`.
|
|
96
|
-
const capsule = globals.capsuleMenu;
|
|
97
|
-
const insets = opts.safeAreaInsets;
|
|
98
|
-
const capsuleCenter = capsule.top + capsule.height / 2;
|
|
99
|
-
const cssVars = {
|
|
100
|
-
...safeAreaCssVars(insets),
|
|
101
|
-
"--glassly-capsule-top": `${capsule.top}px`,
|
|
102
|
-
"--glassly-capsule-bottom": `${capsule.bottom}px`,
|
|
103
|
-
"--glassly-capsule-left": `${capsule.left}px`,
|
|
104
|
-
"--glassly-capsule-right": `${capsule.right}px`,
|
|
105
|
-
"--glassly-capsule-width": `${capsule.width}px`,
|
|
106
|
-
"--glassly-capsule-height": `${capsule.height}px`,
|
|
107
|
-
"--glassly-capsule-center-y": `${capsuleCenter}px`,
|
|
108
|
-
// Right-side gutter to reserve so content doesn't slide under the
|
|
109
|
-
// capsule: capsule width + 16px breathing room.
|
|
110
|
-
"--glassly-capsule-gutter": `${capsule.width + 16}px`,
|
|
111
|
-
};
|
|
79
|
+
const cssVars = layoutCssVars(opts.safeAreaInsets, opts.capsuleMenu);
|
|
112
80
|
const cssVarsBlock = Object.entries(cssVars)
|
|
113
81
|
.map(([k, v]) => `${k}: ${v};`)
|
|
114
82
|
.join(" ");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"miniappGlobals.js","sourceRoot":"","sources":["../../src/utils/miniappGlobals.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * miniappGlobals — shared builder for the window.Glassly globals injected\n * into every miniapp WebView.\n *\n * Miniapps run on-device, and LocalMiniappView is the single injection path;\n * this util is the one place that builds the globals so every WebView gets the\n * same API surface.\n */\n\nimport {Dimensions, Platform} from \"react-native\"\n\nexport interface MiniappSafeArea {\n top: number\n bottom: number\n left: number\n right: number\n}\n\nexport interface CapsuleMenuRect {\n top: number\n right: number\n bottom: number\n left: number\n width: number\n height: number\n}\n\n/**\n * The capsule menu bounding rect in the WebView's own coordinate space.\n *\n * Keep in sync with CapsuleMenu.tsx:\n * - CapsuleButton height ≈ h-7.5 (30px), width ≈ 73px\n * - Positioned at right-2 (8px), top = theme.spacing.s2 (8px) below insets.top\n */\nconst CAPSULE_MENU_HEIGHT = 30\nconst CAPSULE_MENU_WIDTH = 73\nconst CAPSULE_MENU_PADDING = 8\n\n/**\n * @param topInsetOffset extra top offset to add when the WebView container does\n * NOT already pad by the safe-area inset (i.e. WebView fills edge-to-edge under\n * the status bar). Pass `insets.top` in that case, or 0 when the container\n * already applies top padding.\n */\nexport function getCapsuleMenuRect(topInsetOffset = 0): CapsuleMenuRect {\n const screenWidth = Dimensions.get(\"window\").width\n const top = topInsetOffset + CAPSULE_MENU_PADDING\n return {\n top,\n right: CAPSULE_MENU_PADDING,\n bottom: top + CAPSULE_MENU_HEIGHT,\n left: screenWidth - CAPSULE_MENU_PADDING - CAPSULE_MENU_WIDTH,\n width: CAPSULE_MENU_WIDTH,\n height: CAPSULE_MENU_HEIGHT,\n }\n}\n\nexport type MiniappColorScheme = \"light\" | \"dark\"\n\nexport interface BuildMiniappGlobalsOptions {\n packageName?: string\n capabilities?: string[]\n miniappLocal?: boolean\n miniappDeveloperMode?: boolean\n safeAreaInsets: MiniappSafeArea\n /**\n * True when the WebView container renders edge-to-edge (no top padding for\n * the status bar). In that case the capsule menu rect needs to shift down by\n * the top inset so its coords match where it's actually drawn on screen.\n */\n webviewFillsStatusBar?: boolean\n /** Current host color scheme. Miniapps may follow this to match the phone. */\n colorScheme?: MiniappColorScheme\n}\n\nfunction safeAreaCssVars(insets: MiniappSafeArea): Record<string, string> {\n return {\n \"--glassly-safe-top\": `${insets.top}px`,\n \"--glassly-safe-bottom\": `${insets.bottom}px`,\n \"--glassly-safe-left\": `${insets.left}px`,\n \"--glassly-safe-right\": `${insets.right}px`,\n }\n}\n\n/**\n * JS to inject into a running miniapp when the host's safe area changes (an\n * iPhone Duo folding moves its status rail): updates window.Glassly and the\n * CSS variables, then fires `glassly:safe-area-change`.\n */\nexport function buildSafeAreaUpdateScript(insets: MiniappSafeArea): string {\n const sets = Object.entries(safeAreaCssVars(insets))\n .map(([k, v]) => `document.documentElement.style.setProperty(${JSON.stringify(k)}, ${JSON.stringify(v)});`)\n .join(\"\\n \")\n return `\n window.Glassly = window.Glassly || {};\n window.Glassly.safeAreaInsets = ${JSON.stringify(insets)};\n try {\n ${sets}\n } catch (e) { /* ignore */ }\n window.dispatchEvent(new Event(\"glassly:safe-area-change\"));\n true;\n `\n}\n\n/**\n * Returns the JS string to inject into a miniapp WebView before its content\n * loads. Sets window.Glassly with the standard fields miniapps read:\n *\n * window.Glassly = {\n * platform, packageName?, capabilities, safeAreaInsets, capsuleMenu,\n * miniappLocal?, miniappDeveloperMode?\n * }\n *\n * Also defines a stub window.receiveNativeMessage so the miniapp can safely\n * assign to it before the native bridge wires in.\n */\nexport function buildMiniappGlobalsScript(opts: BuildMiniappGlobalsOptions): string {\n const capsuleTopOffset = opts.webviewFillsStatusBar ? opts.safeAreaInsets.top : 0\n const globals: Record<string, unknown> = {\n platform: Platform.OS,\n capabilities: opts.capabilities ?? [\"share\", \"open_url\", \"copy_clipboard\", \"download\"],\n safeAreaInsets: opts.safeAreaInsets,\n capsuleMenu: getCapsuleMenuRect(capsuleTopOffset),\n }\n if (opts.packageName) globals.packageName = opts.packageName\n if (opts.miniappLocal) globals.miniappLocal = true\n if (opts.miniappDeveloperMode) globals.miniappDeveloperMode = true\n if (opts.colorScheme) globals.colorScheme = opts.colorScheme\n\n // CSS custom properties that mirror the capsule menu / safe-area data.\n // Miniapp CSS (or Tailwind arbitrary values) can read these without\n // touching JS: e.g. `style=\"margin-top: var(--glassly-capsule-top)\"`.\n const capsule = globals.capsuleMenu as CapsuleMenuRect\n const insets = opts.safeAreaInsets\n const capsuleCenter = capsule.top + capsule.height / 2\n const cssVars: Record<string, string> = {\n ...safeAreaCssVars(insets),\n \"--glassly-capsule-top\": `${capsule.top}px`,\n \"--glassly-capsule-bottom\": `${capsule.bottom}px`,\n \"--glassly-capsule-left\": `${capsule.left}px`,\n \"--glassly-capsule-right\": `${capsule.right}px`,\n \"--glassly-capsule-width\": `${capsule.width}px`,\n \"--glassly-capsule-height\": `${capsule.height}px`,\n \"--glassly-capsule-center-y\": `${capsuleCenter}px`,\n // Right-side gutter to reserve so content doesn't slide under the\n // capsule: capsule width + 16px breathing room.\n \"--glassly-capsule-gutter\": `${capsule.width + 16}px`,\n }\n const cssVarsBlock = Object.entries(cssVars)\n .map(([k, v]) => `${k}: ${v};`)\n .join(\" \")\n\n // Console-tap shim: wrap console.log/warn/error/info/debug so each call\n // also forwards a `dev_log` envelope back to the phone via\n // ReactNativeWebView.postMessage. Dev mode forwards to the laptop's\n // `glassly dev` terminal; non-dev mode falls back to the React\n // Native log stream (Metro / Xcode console / adb logcat) so installed-\n // miniapp errors are still inspectable when there's no laptop sidecar.\n //\n // Also tap window.onerror and window.onunhandledrejection so synchronous\n // throws and unhandled promise rejections surface even when the bundle\n // never gets a chance to call console.error itself.\n const consoleTapBlock = `\n (function() {\n try {\n var levels = [\"log\", \"warn\", \"error\", \"info\", \"debug\"];\n var seen = new WeakSet();\n function safeSerialize(arg) {\n if (arg instanceof Error) return {__error: true, message: arg.message, stack: arg.stack};\n if (arg === null || typeof arg !== \"object\") return arg;\n try {\n return JSON.parse(JSON.stringify(arg, function(k, v) {\n if (v !== null && typeof v === \"object\") {\n if (seen.has(v)) return \"[Circular]\";\n seen.add(v);\n }\n return v;\n }));\n } catch (e) {\n try { return String(arg); } catch (_) { return \"[unserializable]\"; }\n }\n }\n levels.forEach(function(level) {\n var original = console[level];\n if (typeof original !== \"function\") return;\n console[level] = function() {\n try { original.apply(console, arguments); } catch (_) {}\n try {\n var args = Array.prototype.slice.call(arguments).map(safeSerialize);\n if (window.ReactNativeWebView && typeof window.ReactNativeWebView.postMessage === \"function\") {\n window.ReactNativeWebView.postMessage(JSON.stringify({\n payload: {\n type: \"dev_log\",\n level: level,\n args: args,\n packageName: (window.Glassly && window.Glassly.packageName) || null,\n timestamp: Date.now()\n }\n }));\n }\n } catch (_) { /* swallow */ }\n };\n });\n } catch (_) { /* ignore */ }\n\n // Surface synchronous errors and unhandled promise rejections via the\n // same dev_log channel — many bundle-startup crashes never get to call\n // console.error themselves.\n try {\n window.addEventListener(\"error\", function(e) {\n try {\n if (window.ReactNativeWebView && typeof window.ReactNativeWebView.postMessage === \"function\") {\n window.ReactNativeWebView.postMessage(JSON.stringify({\n payload: {\n type: \"dev_log\",\n level: \"error\",\n args: [\"[window.error] \" + (e.message || \"unknown\"), {filename: e.filename, lineno: e.lineno, colno: e.colno, stack: e.error && e.error.stack}],\n packageName: (window.Glassly && window.Glassly.packageName) || null,\n timestamp: Date.now()\n }\n }));\n }\n } catch (_) {}\n });\n window.addEventListener(\"unhandledrejection\", function(e) {\n try {\n if (window.ReactNativeWebView && typeof window.ReactNativeWebView.postMessage === \"function\") {\n var reason = e.reason;\n var serialized = reason instanceof Error\n ? {__error: true, message: reason.message, stack: reason.stack}\n : (typeof reason === \"object\" ? JSON.stringify(reason) : String(reason));\n window.ReactNativeWebView.postMessage(JSON.stringify({\n payload: {\n type: \"dev_log\",\n level: \"error\",\n args: [\"[unhandledrejection]\", serialized],\n packageName: (window.Glassly && window.Glassly.packageName) || null,\n timestamp: Date.now()\n }\n }));\n }\n } catch (_) {}\n });\n } catch (_) {}\n })();\n `\n\n // Lock zoom so miniapps feel like apps, not pages: force a non-scalable\n // viewport meta (overriding whatever the miniapp shipped) and disable\n // double-tap-zoom via CSS. Re-applies on DOMContentLoaded in case the\n // miniapp re-writes <head> during boot.\n return `\n window.Glassly = ${JSON.stringify(globals)};\n window.receiveNativeMessage = window.receiveNativeMessage || function() {};\n (function() {\n try {\n var styleEl = document.createElement(\"style\");\n styleEl.setAttribute(\"data-glassly-injected\", \"1\");\n styleEl.textContent =\n \":root { ${cssVarsBlock} }\" +\n \"html, body { touch-action: manipulation; -ms-content-zooming: none; }\";\n (document.head || document.documentElement).appendChild(styleEl);\n } catch (e) { /* ignore */ }\n\n function lockViewport() {\n try {\n var content = \"width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no, viewport-fit=cover\";\n var existing = document.querySelector('meta[name=\"viewport\"]');\n if (existing) {\n existing.setAttribute(\"content\", content);\n } else {\n var meta = document.createElement(\"meta\");\n meta.setAttribute(\"name\", \"viewport\");\n meta.setAttribute(\"content\", content);\n (document.head || document.documentElement).appendChild(meta);\n }\n } catch (e) { /* ignore */ }\n }\n lockViewport();\n document.addEventListener(\"DOMContentLoaded\", lockViewport);\n })();\n ${consoleTapBlock}\n true;\n `\n}\n"]}
|
|
1
|
+
{"version":3,"file":"miniappGlobals.js","sourceRoot":"","sources":["../../src/utils/miniappGlobals.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * miniappGlobals — shared builder for the window.Glassly globals injected\n * into every miniapp WebView.\n *\n * Miniapps run on-device, and LocalMiniappView is the single injection path;\n * this util is the one place that builds the globals so every WebView gets the\n * same API surface.\n */\n\nimport {Platform} from \"react-native\"\n\nexport interface MiniappSafeArea {\n top: number\n bottom: number\n left: number\n right: number\n}\n\n/**\n * The host's capsule menu in the WebView's own coordinate space. `corner`\n * floats over the page's top-right; `rail` sits in the iPhone Duo's status\n * rail, inside the side safe-area inset and clear of the page content.\n */\nexport interface CapsuleMenuRect {\n top: number\n right: number\n bottom: number\n left: number\n width: number\n height: number\n placement: \"corner\" | \"rail\"\n}\n\nexport type MiniappColorScheme = \"light\" | \"dark\"\n\nexport interface BuildMiniappGlobalsOptions {\n packageName?: string\n capabilities?: string[]\n miniappLocal?: boolean\n miniappDeveloperMode?: boolean\n safeAreaInsets: MiniappSafeArea\n /** Where the host draws its capsule menu over this WebView. */\n capsuleMenu: CapsuleMenuRect\n /** Current host color scheme. Miniapps may follow this to match the phone. */\n colorScheme?: MiniappColorScheme\n}\n\n// CSS custom properties that mirror the safe-area and capsule menu data, so\n// miniapp CSS (or Tailwind arbitrary values) can read them without touching\n// JS: e.g. `style=\"margin-top: var(--glassly-capsule-top)\"`.\nfunction layoutCssVars(insets: MiniappSafeArea, capsule: CapsuleMenuRect): Record<string, string> {\n return {\n \"--glassly-safe-top\": `${insets.top}px`,\n \"--glassly-safe-bottom\": `${insets.bottom}px`,\n \"--glassly-safe-left\": `${insets.left}px`,\n \"--glassly-safe-right\": `${insets.right}px`,\n \"--glassly-capsule-top\": `${capsule.top}px`,\n \"--glassly-capsule-bottom\": `${capsule.bottom}px`,\n \"--glassly-capsule-left\": `${capsule.left}px`,\n \"--glassly-capsule-right\": `${capsule.right}px`,\n \"--glassly-capsule-width\": `${capsule.width}px`,\n \"--glassly-capsule-height\": `${capsule.height}px`,\n \"--glassly-capsule-center-y\": `${capsule.top + capsule.height / 2}px`,\n // Right-side gutter to reserve so content doesn't slide under the\n // capsule: its width + 16px breathing room, none when it sits in the rail.\n \"--glassly-capsule-gutter\": `${capsule.placement === \"rail\" ? 0 : capsule.width + 16}px`,\n }\n}\n\n/**\n * JS to inject into a running miniapp when the host's safe area or capsule\n * menu moves (an iPhone Duo folding, or its rail's reserved regions growing):\n * updates window.Glassly and the CSS variables, then fires\n * `glassly:safe-area-change`.\n */\nexport function buildSafeAreaUpdateScript(insets: MiniappSafeArea, capsuleMenu: CapsuleMenuRect): string {\n const sets = Object.entries(layoutCssVars(insets, capsuleMenu))\n .map(([k, v]) => `document.documentElement.style.setProperty(${JSON.stringify(k)}, ${JSON.stringify(v)});`)\n .join(\"\\n \")\n return `\n window.Glassly = window.Glassly || {};\n window.Glassly.safeAreaInsets = ${JSON.stringify(insets)};\n window.Glassly.capsuleMenu = ${JSON.stringify(capsuleMenu)};\n try {\n ${sets}\n } catch (e) { /* ignore */ }\n window.dispatchEvent(new Event(\"glassly:safe-area-change\"));\n true;\n `\n}\n\n/**\n * Returns the JS string to inject into a miniapp WebView before its content\n * loads. Sets window.Glassly with the standard fields miniapps read:\n *\n * window.Glassly = {\n * platform, packageName?, capabilities, safeAreaInsets, capsuleMenu,\n * miniappLocal?, miniappDeveloperMode?\n * }\n *\n * Also defines a stub window.receiveNativeMessage so the miniapp can safely\n * assign to it before the native bridge wires in.\n */\nexport function buildMiniappGlobalsScript(opts: BuildMiniappGlobalsOptions): string {\n const globals: Record<string, unknown> = {\n platform: Platform.OS,\n capabilities: opts.capabilities ?? [\"share\", \"open_url\", \"copy_clipboard\", \"download\"],\n safeAreaInsets: opts.safeAreaInsets,\n capsuleMenu: opts.capsuleMenu,\n }\n if (opts.packageName) globals.packageName = opts.packageName\n if (opts.miniappLocal) globals.miniappLocal = true\n if (opts.miniappDeveloperMode) globals.miniappDeveloperMode = true\n if (opts.colorScheme) globals.colorScheme = opts.colorScheme\n\n const cssVars = layoutCssVars(opts.safeAreaInsets, opts.capsuleMenu)\n const cssVarsBlock = Object.entries(cssVars)\n .map(([k, v]) => `${k}: ${v};`)\n .join(\" \")\n\n // Console-tap shim: wrap console.log/warn/error/info/debug so each call\n // also forwards a `dev_log` envelope back to the phone via\n // ReactNativeWebView.postMessage. Dev mode forwards to the laptop's\n // `glassly dev` terminal; non-dev mode falls back to the React\n // Native log stream (Metro / Xcode console / adb logcat) so installed-\n // miniapp errors are still inspectable when there's no laptop sidecar.\n //\n // Also tap window.onerror and window.onunhandledrejection so synchronous\n // throws and unhandled promise rejections surface even when the bundle\n // never gets a chance to call console.error itself.\n const consoleTapBlock = `\n (function() {\n try {\n var levels = [\"log\", \"warn\", \"error\", \"info\", \"debug\"];\n var seen = new WeakSet();\n function safeSerialize(arg) {\n if (arg instanceof Error) return {__error: true, message: arg.message, stack: arg.stack};\n if (arg === null || typeof arg !== \"object\") return arg;\n try {\n return JSON.parse(JSON.stringify(arg, function(k, v) {\n if (v !== null && typeof v === \"object\") {\n if (seen.has(v)) return \"[Circular]\";\n seen.add(v);\n }\n return v;\n }));\n } catch (e) {\n try { return String(arg); } catch (_) { return \"[unserializable]\"; }\n }\n }\n levels.forEach(function(level) {\n var original = console[level];\n if (typeof original !== \"function\") return;\n console[level] = function() {\n try { original.apply(console, arguments); } catch (_) {}\n try {\n var args = Array.prototype.slice.call(arguments).map(safeSerialize);\n if (window.ReactNativeWebView && typeof window.ReactNativeWebView.postMessage === \"function\") {\n window.ReactNativeWebView.postMessage(JSON.stringify({\n payload: {\n type: \"dev_log\",\n level: level,\n args: args,\n packageName: (window.Glassly && window.Glassly.packageName) || null,\n timestamp: Date.now()\n }\n }));\n }\n } catch (_) { /* swallow */ }\n };\n });\n } catch (_) { /* ignore */ }\n\n // Surface synchronous errors and unhandled promise rejections via the\n // same dev_log channel — many bundle-startup crashes never get to call\n // console.error themselves.\n try {\n window.addEventListener(\"error\", function(e) {\n try {\n if (window.ReactNativeWebView && typeof window.ReactNativeWebView.postMessage === \"function\") {\n window.ReactNativeWebView.postMessage(JSON.stringify({\n payload: {\n type: \"dev_log\",\n level: \"error\",\n args: [\"[window.error] \" + (e.message || \"unknown\"), {filename: e.filename, lineno: e.lineno, colno: e.colno, stack: e.error && e.error.stack}],\n packageName: (window.Glassly && window.Glassly.packageName) || null,\n timestamp: Date.now()\n }\n }));\n }\n } catch (_) {}\n });\n window.addEventListener(\"unhandledrejection\", function(e) {\n try {\n if (window.ReactNativeWebView && typeof window.ReactNativeWebView.postMessage === \"function\") {\n var reason = e.reason;\n var serialized = reason instanceof Error\n ? {__error: true, message: reason.message, stack: reason.stack}\n : (typeof reason === \"object\" ? JSON.stringify(reason) : String(reason));\n window.ReactNativeWebView.postMessage(JSON.stringify({\n payload: {\n type: \"dev_log\",\n level: \"error\",\n args: [\"[unhandledrejection]\", serialized],\n packageName: (window.Glassly && window.Glassly.packageName) || null,\n timestamp: Date.now()\n }\n }));\n }\n } catch (_) {}\n });\n } catch (_) {}\n })();\n `\n\n // Lock zoom so miniapps feel like apps, not pages: force a non-scalable\n // viewport meta (overriding whatever the miniapp shipped) and disable\n // double-tap-zoom via CSS. Re-applies on DOMContentLoaded in case the\n // miniapp re-writes <head> during boot.\n return `\n window.Glassly = ${JSON.stringify(globals)};\n window.receiveNativeMessage = window.receiveNativeMessage || function() {};\n (function() {\n try {\n var styleEl = document.createElement(\"style\");\n styleEl.setAttribute(\"data-glassly-injected\", \"1\");\n styleEl.textContent =\n \":root { ${cssVarsBlock} }\" +\n \"html, body { touch-action: manipulation; -ms-content-zooming: none; }\";\n (document.head || document.documentElement).appendChild(styleEl);\n } catch (e) { /* ignore */ }\n\n function lockViewport() {\n try {\n var content = \"width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no, viewport-fit=cover\";\n var existing = document.querySelector('meta[name=\"viewport\"]');\n if (existing) {\n existing.setAttribute(\"content\", content);\n } else {\n var meta = document.createElement(\"meta\");\n meta.setAttribute(\"name\", \"viewport\");\n meta.setAttribute(\"content\", content);\n (document.head || document.documentElement).appendChild(meta);\n }\n } catch (e) { /* ignore */ }\n }\n lockViewport();\n document.addEventListener(\"DOMContentLoaded\", lockViewport);\n })();\n ${consoleTapBlock}\n true;\n `\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glassly/engine",
|
|
3
|
-
"version": "0.3.7-beta.
|
|
3
|
+
"version": "0.3.7-beta.896",
|
|
4
4
|
"description": "Glassly Engine — miniapp registry, WebView bridge, and miniapp store",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"react-native": "src/index.ts",
|
|
@@ -97,17 +97,17 @@
|
|
|
97
97
|
"registry": "https://registry.npmjs.org/"
|
|
98
98
|
},
|
|
99
99
|
"dependencies": {
|
|
100
|
-
"@glassly/bluetooth-sdk": "0.3.7-beta.
|
|
101
|
-
"@glassly/cloud-client": "0.3.7-beta.
|
|
102
|
-
"@glassly/cloud-protocol": "0.3.7-beta.
|
|
103
|
-
"@glassly/crust": "0.3.7-beta.
|
|
104
|
-
"@glassly/miniapp": "0.3.7-beta.
|
|
100
|
+
"@glassly/bluetooth-sdk": "0.3.7-beta.896",
|
|
101
|
+
"@glassly/cloud-client": "0.3.7-beta.896",
|
|
102
|
+
"@glassly/cloud-protocol": "0.3.7-beta.896",
|
|
103
|
+
"@glassly/crust": "0.3.7-beta.896",
|
|
104
|
+
"@glassly/miniapp": "0.3.7-beta.896",
|
|
105
105
|
"buffer": "^6.0.3",
|
|
106
106
|
"events": "^3.3.0",
|
|
107
107
|
"react-native-marked": "8.3.2",
|
|
108
108
|
"semver": "^7.7.4",
|
|
109
109
|
"typesafe-ts": "^1.7.1",
|
|
110
|
-
"@glassly/llm-providers": "0.3.7-beta.
|
|
110
|
+
"@glassly/llm-providers": "0.3.7-beta.896",
|
|
111
111
|
"@huggingface/jinja": "^0.5.9",
|
|
112
112
|
"jsonrepair": "^3.12.0"
|
|
113
113
|
},
|
package/src/engine.ts
CHANGED
|
@@ -49,6 +49,7 @@ import {permissions} from "./facades/permissions"
|
|
|
49
49
|
import {notifications} from "./facades/notifications"
|
|
50
50
|
import {maps} from "./facades/maps"
|
|
51
51
|
import {memory} from "./facades/memory"
|
|
52
|
+
import {backup} from "./facades/backup"
|
|
52
53
|
import {speakers} from "./facades/speakers"
|
|
53
54
|
import {providerKeys} from "./facades/providerKeys"
|
|
54
55
|
import {logBuffer} from "./utils/devLogging"
|
|
@@ -232,6 +233,8 @@ export const engine = {
|
|
|
232
233
|
maps,
|
|
233
234
|
/** Shared user-scoped memory timeline (Settings > Memory + session.memory). */
|
|
234
235
|
memory,
|
|
236
|
+
/** Whole-app backup file: settings, miniapp storage, installed list, memory (Settings > Profile). */
|
|
237
|
+
backup,
|
|
235
238
|
/** Device-local voice profiles for on-device speaker ID (Settings > Speakers). */
|
|
236
239
|
speakers,
|
|
237
240
|
/** AI/voice provider key validation — user-supplied keys (Settings > API Keys). */
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* backup facade — `engine.backup`: the whole-app backup behind Settings >
|
|
3
|
+
* Profile > Backup. One JSON file carries system settings, every miniapp's
|
|
4
|
+
* storage (so their sign-ins survive a logout or reinstall), the installed
|
|
5
|
+
* miniapp list, and the memory timeline.
|
|
6
|
+
*/
|
|
7
|
+
import {AppStateBackupError} from "../services/AppStateBackup"
|
|
8
|
+
import {
|
|
9
|
+
exportAppStateFile,
|
|
10
|
+
importAppStateFile,
|
|
11
|
+
type AppStateExportResult,
|
|
12
|
+
type AppStateRestoreOptions,
|
|
13
|
+
type AppStateRestoreResult,
|
|
14
|
+
} from "../services/appStateBackupFile"
|
|
15
|
+
import {MemoryBackupError} from "../services/MemoryStore"
|
|
16
|
+
|
|
17
|
+
export const backup = {
|
|
18
|
+
/** Share the app state as a JSON backup file. */
|
|
19
|
+
exportFile: (): Promise<AppStateExportResult> => exportAppStateFile(),
|
|
20
|
+
/**
|
|
21
|
+
* Pick a backup file and restore it: backup values overwrite per key, and
|
|
22
|
+
* running miniapps whose storage changed restart after `opts.reinstall` has
|
|
23
|
+
* installed the ones this phone lacks. Resolves null on cancel.
|
|
24
|
+
*/
|
|
25
|
+
importFile: (opts?: AppStateRestoreOptions): Promise<AppStateRestoreResult | null> => importAppStateFile(opts),
|
|
26
|
+
/** True for a file that isn't a backup this build can read. */
|
|
27
|
+
isBackupError: (err: unknown): boolean => err instanceof AppStateBackupError || err instanceof MemoryBackupError,
|
|
28
|
+
}
|
|
@@ -12,9 +12,9 @@ export interface EngineReleaseMetadata {
|
|
|
12
12
|
export const ENGINE_RELEASE_METADATA: Readonly<EngineReleaseMetadata> = Object.freeze({
|
|
13
13
|
"schemaVersion": 1,
|
|
14
14
|
"familyBaseVersion": "0.3.7",
|
|
15
|
-
"releaseIdentity": "0.3.7-beta.
|
|
16
|
-
"releaseSetId": "glassly-0.3.7-beta.
|
|
17
|
-
"sourceCommit": "
|
|
15
|
+
"releaseIdentity": "0.3.7-beta.896",
|
|
16
|
+
"releaseSetId": "glassly-0.3.7-beta.896",
|
|
17
|
+
"sourceCommit": "251fd15bfc9bfedd0d7ad78b78bf2843767d80d6",
|
|
18
18
|
"otaManifestUrl": null,
|
|
19
19
|
"otaManifestSha256": null
|
|
20
20
|
})
|
package/src/index.ts
CHANGED
|
@@ -69,6 +69,7 @@ export {DEFAULT_LOCAL_LLM_MODEL, isLocalLlmModelId} from "./services/LocalLlmMod
|
|
|
69
69
|
// session shapes are canonical in @glassly/miniapp (the session.memory
|
|
70
70
|
// surface); the stats read model is engine-side.
|
|
71
71
|
export type {MemoryStats} from "./services/MemoryStore"
|
|
72
|
+
export type {AppStateExportResult, AppStateRestoreOptions, AppStateRestoreResult} from "./services/appStateBackupFile"
|
|
72
73
|
export type {SpeakerProfile} from "./services/SpeakerProfileStore"
|
|
73
74
|
export type {EnrollOutcome} from "./facades/speakers"
|
|
74
75
|
export {MENTRA_LIVE_SETTING_KEYS, getBluetoothSettingKeysForDevice} from "./stores/bluetoothSettingKeys"
|
|
@@ -658,7 +658,8 @@ class AppRegistry {
|
|
|
658
658
|
}
|
|
659
659
|
}
|
|
660
660
|
|
|
661
|
-
|
|
661
|
+
/** Undefined when no bundle is on disk (offline built-ins, HTTP-direct dev apps). */
|
|
662
|
+
public async getActiveVersion(packageName: string): Promise<string | undefined> {
|
|
662
663
|
let versions = this.getInstalledVersions(packageName)
|
|
663
664
|
// Treat MMKV as a hint, not authority. A stored version may have been
|
|
664
665
|
// GC'd off disk without this pointer being updated.
|
|
@@ -677,8 +678,10 @@ class AppRegistry {
|
|
|
677
678
|
}
|
|
678
679
|
versions = versions.filter((v) => semver.valid(v))
|
|
679
680
|
versions.sort((a, b) => semver.rcompare(a, b))
|
|
680
|
-
|
|
681
|
-
|
|
681
|
+
const latest = versions[0] as string | undefined
|
|
682
|
+
// Never persist undefined: MMKV.set throws on it.
|
|
683
|
+
if (latest) this.setActiveVersion(packageName, latest)
|
|
684
|
+
return latest
|
|
682
685
|
}
|
|
683
686
|
|
|
684
687
|
public setActiveVersion(packageName: string, version: string): Result<void, Error> {
|
|
@@ -747,6 +750,8 @@ class AppRegistry {
|
|
|
747
750
|
const out: ClientApp[] = []
|
|
748
751
|
for (const lmaInfo of installedInfo) {
|
|
749
752
|
const versionString = await this.getActiveVersion(lmaInfo.packageName)
|
|
753
|
+
// A package dir with no usable bundle (e.g. only a half-written version) has nothing to run.
|
|
754
|
+
if (!versionString) continue
|
|
750
755
|
const versionInfo = lmaInfo.versions[versionString]
|
|
751
756
|
|
|
752
757
|
const manifest = this.getMiniappManifest(lmaInfo.packageName, versionString) as {
|