akanjs 2.3.9 → 2.3.10-rc.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/capacitor.base.config.ts +14 -1
- package/client/capacitor.ts +1 -0
- package/client/router.ts +132 -1
- package/index.ts +11 -4
- package/package.json +1 -1
- package/server/proxy/localeWebProxy.ts +5 -1
- package/server/types.tsx +9 -0
- package/server/webRouter.ts +53 -0
- package/types/client/capacitor.d.ts +3 -0
- package/types/client/router.d.ts +13 -0
- package/types/index.d.ts +11 -4
- package/types/server/types.d.ts +8 -0
- package/types/ui/System/Client.d.ts +3 -0
- package/types/webkit/bootCsr.d.ts +1 -0
- package/ui/Dialog/Modal.tsx +32 -7
- package/ui/System/CSR.tsx +9 -1
- package/ui/System/Client.tsx +4 -0
- package/webkit/bootCsr.tsx +3 -2
- package/webkit/useCsrValues.ts +81 -1
package/capacitor.base.config.ts
CHANGED
|
@@ -85,6 +85,7 @@ const localCsrUrl = (ip: string, target: AkanMobileTargetConfig, appInfo: AppSca
|
|
|
85
85
|
const port = resolveLocalCsrPort(appInfo);
|
|
86
86
|
const params = new URLSearchParams({ csr: "true", akanMobileTarget: target.name });
|
|
87
87
|
if (basePath) params.set("akanMobileBasePath", basePath);
|
|
88
|
+
if (target.indexPath) params.set("akanMobileIndexPath", target.indexPath);
|
|
88
89
|
return `http://${ip}:${port}/${pathname}?${params}`;
|
|
89
90
|
};
|
|
90
91
|
|
|
@@ -97,8 +98,20 @@ export const withBase = (
|
|
|
97
98
|
const appInfo = appData;
|
|
98
99
|
if (!appInfo) throw new Error("withBase requires apps/<app>/akan.app.json metadata.");
|
|
99
100
|
const target = resolveTarget(appInfo, targetName);
|
|
101
|
+
const {
|
|
102
|
+
name: _name,
|
|
103
|
+
basePath: _basePath,
|
|
104
|
+
indexPath: _indexPath,
|
|
105
|
+
version: _version,
|
|
106
|
+
buildNum: _buildNum,
|
|
107
|
+
assets: _assets,
|
|
108
|
+
permissions: _permissions,
|
|
109
|
+
deepLinks: _deepLinks,
|
|
110
|
+
files: _files,
|
|
111
|
+
...capacitorTarget
|
|
112
|
+
} = target;
|
|
100
113
|
const baseConfig: CapacitorConfig = {
|
|
101
|
-
...
|
|
114
|
+
...capacitorTarget,
|
|
102
115
|
appId: target.appId,
|
|
103
116
|
appName: target.appName,
|
|
104
117
|
webDir: "dist",
|
package/client/capacitor.ts
CHANGED
|
@@ -16,6 +16,7 @@ export type CapacitorAppModule = {
|
|
|
16
16
|
addListener: (eventName: string, listenerFunc: (...args: unknown[]) => void) => Promise<unknown> | unknown;
|
|
17
17
|
removeAllListeners: () => Promise<void> | void;
|
|
18
18
|
exitApp?: () => Promise<void> | void;
|
|
19
|
+
getLaunchUrl?: () => Promise<{ url?: string | null }>;
|
|
19
20
|
getInfo: () => Promise<{ id: string; version: string; build: string; [key: string]: unknown }>;
|
|
20
21
|
};
|
|
21
22
|
};
|
package/client/router.ts
CHANGED
|
@@ -4,6 +4,14 @@ import { getBasePathFromPathname, Logger, parseAkanI18nEnv, parseBasePaths } fro
|
|
|
4
4
|
export interface RouteOptions {
|
|
5
5
|
scrollToTop?: boolean;
|
|
6
6
|
}
|
|
7
|
+
export interface RouteManifest {
|
|
8
|
+
routes: string[];
|
|
9
|
+
}
|
|
10
|
+
export interface DeepLinkOptions extends RouteOptions {
|
|
11
|
+
resetStack?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const DEEP_LINK_STACK_STEP_DELAY = 450;
|
|
7
15
|
|
|
8
16
|
export interface RouterInstance {
|
|
9
17
|
push: (href: string, routeOptions?: RouteOptions) => void;
|
|
@@ -20,6 +28,8 @@ interface InternalRouterInstance {
|
|
|
20
28
|
interface RouterOptions {
|
|
21
29
|
prefix?: string;
|
|
22
30
|
lang?: string;
|
|
31
|
+
routeManifest?: RouteManifest | string[];
|
|
32
|
+
indexPath?: string;
|
|
23
33
|
}
|
|
24
34
|
interface SsrServerRouterOption extends RouterOptions {
|
|
25
35
|
type: "ssr";
|
|
@@ -72,13 +82,44 @@ function getServerRequestContext() {
|
|
|
72
82
|
const getConfiguredBasePaths = () => new Set(parseBasePaths(process.env.AKAN_PUBLIC_BASE_PATHS));
|
|
73
83
|
|
|
74
84
|
const shouldExposeBasePath = () => getEnv().operationMode === "local";
|
|
75
|
-
const CSR_RUNTIME_SEARCH_PARAMS = ["csr", "akanMobileTarget", "akanMobileBasePath"] as const;
|
|
85
|
+
const CSR_RUNTIME_SEARCH_PARAMS = ["csr", "akanMobileTarget", "akanMobileBasePath", "akanMobileIndexPath"] as const;
|
|
76
86
|
|
|
77
87
|
const getLocaleFromPathname = (pathname: string) => {
|
|
78
88
|
const [firstSegment] = pathname.split("/").filter(Boolean);
|
|
79
89
|
return parseAkanI18nEnv().locales.find((locale) => locale === firstSegment);
|
|
80
90
|
};
|
|
81
91
|
|
|
92
|
+
const normalizeRoutePath = (path: string | undefined) => {
|
|
93
|
+
const normalized = path?.trim();
|
|
94
|
+
if (!normalized) return undefined;
|
|
95
|
+
const [pathWithoutHash, hash = ""] = normalized.split("#");
|
|
96
|
+
const [pathname, search = ""] = pathWithoutHash.split("?");
|
|
97
|
+
const routePath = `/${pathname.replace(/^\/+|\/+$/g, "")}`;
|
|
98
|
+
return `${routePath === "/" ? "/" : routePath}${search ? `?${search}` : ""}${hash ? `#${hash}` : ""}`;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const splitHref = (href: string) => {
|
|
102
|
+
const [hrefWithoutHash, hash = ""] = href.split("#");
|
|
103
|
+
const [path, search = ""] = hrefWithoutHash.split("?");
|
|
104
|
+
return { path: normalizeRoutePath(path) ?? "/", search, hash };
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const normalizeRouteManifestPath = (href: string, prefix = "") => {
|
|
108
|
+
const { path } = splitHref(href);
|
|
109
|
+
const segments = path.split("/").filter(Boolean);
|
|
110
|
+
if (segments[0] === ":lang") segments.shift();
|
|
111
|
+
if (prefix && segments[0] === prefix) segments.shift();
|
|
112
|
+
return segments.length === 0 ? "/" : `/${segments.join("/")}`;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
export const normalizeDeepLinkHref = (href: string, origin = globalThis.window?.location?.origin ?? "http://localhost") => {
|
|
116
|
+
const url = new URL(href, origin);
|
|
117
|
+
if (url.protocol === "http:" || url.protocol === "https:") return `${url.pathname}${url.search}${url.hash}`;
|
|
118
|
+
const hostPath = url.hostname ? `/${url.hostname}` : "";
|
|
119
|
+
const pathname = `${hostPath}${url.pathname === "/" ? "" : url.pathname}`;
|
|
120
|
+
return `${normalizeRoutePath(pathname) ?? "/"}${url.search}${url.hash}`;
|
|
121
|
+
};
|
|
122
|
+
|
|
82
123
|
const getServerBasePath = (reqPathname: string, lang: string, headerBasePath: string | undefined, fallback: string) => {
|
|
83
124
|
return (
|
|
84
125
|
getBasePathFromPathname(reqPathname, {
|
|
@@ -117,6 +158,9 @@ class Router {
|
|
|
117
158
|
isInitialized = false;
|
|
118
159
|
#prefix = "";
|
|
119
160
|
#lang = parseAkanI18nEnv().defaultLocale;
|
|
161
|
+
#routePaths = new Set<string>();
|
|
162
|
+
#indexPath = "/";
|
|
163
|
+
#historyIdx = 0;
|
|
120
164
|
#instance: InternalRouterInstance = {
|
|
121
165
|
push: (href: string) => {
|
|
122
166
|
const { href: fullHref } = this.#getPathInfo(href);
|
|
@@ -137,6 +181,17 @@ class Router {
|
|
|
137
181
|
|
|
138
182
|
this.#prefix = options.prefix ?? "";
|
|
139
183
|
this.#lang = options.lang ?? parseAkanI18nEnv().defaultLocale;
|
|
184
|
+
this.#routePaths = new Set(
|
|
185
|
+
(Array.isArray(options.routeManifest) ? options.routeManifest : (options.routeManifest?.routes ?? []))
|
|
186
|
+
.map((path) => normalizeRouteManifestPath(path, this.#prefix))
|
|
187
|
+
.filter(Boolean),
|
|
188
|
+
);
|
|
189
|
+
this.#indexPath = splitHref(options.indexPath ?? "/").path;
|
|
190
|
+
if (this.#routePaths.size > 0 && !this.#routePaths.has(this.#indexPath)) {
|
|
191
|
+
Logger.log(`[router] indexPath '${this.#indexPath}' was not found in route manifest. Falling back to '/'.`);
|
|
192
|
+
this.#indexPath = "/";
|
|
193
|
+
}
|
|
194
|
+
this.#ensureHistoryState();
|
|
140
195
|
if (options.type === "csr") this.#initCsrClientRouter(options);
|
|
141
196
|
else if (options.side === "server") this.#initSsrServerRouter(options);
|
|
142
197
|
else this.#initSsrClientRouter(options);
|
|
@@ -206,6 +261,31 @@ class Router {
|
|
|
206
261
|
if (!this.isInitialized) throw new Error("Router is not initialized");
|
|
207
262
|
}
|
|
208
263
|
|
|
264
|
+
#ensureHistoryState() {
|
|
265
|
+
if (getEnv().side === "server") return;
|
|
266
|
+
const history = globalThis.window?.history;
|
|
267
|
+
if (!history?.replaceState) return;
|
|
268
|
+
const state = (history.state ?? {}) as Record<string, unknown>;
|
|
269
|
+
const akanState = (state.__akanRouter ?? {}) as { idx?: number };
|
|
270
|
+
this.#historyIdx = Number.isInteger(akanState.idx) ? (akanState.idx as number) : this.#historyIdx;
|
|
271
|
+
history.replaceState({ ...state, __akanRouter: { ...akanState, idx: this.#historyIdx } }, "", globalThis.window.location.href);
|
|
272
|
+
if (!globalThis.window.addEventListener) return;
|
|
273
|
+
globalThis.window.addEventListener("popstate", (event) => {
|
|
274
|
+
const nextState = ((event.state as Record<string, unknown> | null)?.__akanRouter ?? {}) as { idx?: number };
|
|
275
|
+
if (Number.isInteger(nextState.idx)) this.#historyIdx = nextState.idx as number;
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
#rememberHistoryState(kind: "push" | "replace") {
|
|
280
|
+
if (getEnv().side === "server") return;
|
|
281
|
+
const history = globalThis.window?.history;
|
|
282
|
+
if (!history) return;
|
|
283
|
+
if (kind === "push") this.#historyIdx += 1;
|
|
284
|
+
const state = (history.state ?? {}) as Record<string, unknown>;
|
|
285
|
+
const nextState = { ...state, __akanRouter: { ...((state.__akanRouter ?? {}) as object), idx: this.#historyIdx } };
|
|
286
|
+
if (history.replaceState) history.replaceState(nextState, "", globalThis.window.location.href);
|
|
287
|
+
}
|
|
288
|
+
|
|
209
289
|
#getPathInfo(href: string, prefix = this.#prefix) {
|
|
210
290
|
return getPathInfo(href, this.#lang, prefix);
|
|
211
291
|
}
|
|
@@ -239,18 +319,69 @@ class Router {
|
|
|
239
319
|
const { path, search, hash } = this.#getPathInfo(href);
|
|
240
320
|
globalThis.__AKAN_DEV_SYNC_NAVIGATION__?.(`${path}${search ? `?${search}` : ""}${hash ? `#${hash}` : ""}`, kind);
|
|
241
321
|
}
|
|
322
|
+
#getExistingSegmentStack(path: string) {
|
|
323
|
+
const segments = path.split("/").filter(Boolean);
|
|
324
|
+
if (segments.length === 0) return this.#routePaths.has("/") ? ["/"] : [];
|
|
325
|
+
return segments
|
|
326
|
+
.map((_, index) => `/${segments.slice(0, index + 1).join("/")}`)
|
|
327
|
+
.filter((candidate) => this.#routePaths.has(candidate));
|
|
328
|
+
}
|
|
329
|
+
resolveDeepLinkStack(href: string) {
|
|
330
|
+
this.#checkInitialized();
|
|
331
|
+
const normalizedHref = normalizeDeepLinkHref(href);
|
|
332
|
+
const { path, search, hash } = splitHref(normalizedHref);
|
|
333
|
+
if (this.#routePaths.size > 0 && !this.#routePaths.has(path)) {
|
|
334
|
+
Logger.log(`[router] deep link target '${path}' was not found in route manifest.`);
|
|
335
|
+
return [];
|
|
336
|
+
}
|
|
337
|
+
const stack = this.#routePaths.size > 0 ? this.#getExistingSegmentStack(path) : [path];
|
|
338
|
+
const baseStack =
|
|
339
|
+
stack.length > 1 || this.#indexPath === path || stack.includes(this.#indexPath) ? stack : [this.#indexPath, ...stack];
|
|
340
|
+
const dedupedStack = baseStack.filter((candidate, index) => baseStack.indexOf(candidate) === index);
|
|
341
|
+
const target = `${path}${search ? `?${search}` : ""}${hash ? `#${hash}` : ""}`;
|
|
342
|
+
return dedupedStack.map((candidate, index) => (index === dedupedStack.length - 1 ? target : candidate));
|
|
343
|
+
}
|
|
344
|
+
enterDeepLink(href: string, options: DeepLinkOptions = {}) {
|
|
345
|
+
this.#checkInitialized();
|
|
346
|
+
if (!options.resetStack) {
|
|
347
|
+
this.push(normalizeDeepLinkHref(href), options);
|
|
348
|
+
return true;
|
|
349
|
+
}
|
|
350
|
+
const stack = this.resolveDeepLinkStack(href);
|
|
351
|
+
if (stack.length === 0) return false;
|
|
352
|
+
const { resetStack: _resetStack, ...routeOptions } = options;
|
|
353
|
+
this.replace(stack[0], routeOptions);
|
|
354
|
+
stack.slice(1).forEach((target, index) => {
|
|
355
|
+
setTimeout(() => this.push(target, routeOptions), DEEP_LINK_STACK_STEP_DELAY * (index + 1));
|
|
356
|
+
});
|
|
357
|
+
return true;
|
|
358
|
+
}
|
|
242
359
|
push(href: string, routeOptions?: RouteOptions) {
|
|
243
360
|
this.#checkInitialized();
|
|
244
361
|
this.#instance.push(href, routeOptions);
|
|
362
|
+
this.#rememberHistoryState("push");
|
|
245
363
|
this.#postDevSyncNavigation("push", href);
|
|
246
364
|
return undefined as never;
|
|
247
365
|
}
|
|
248
366
|
replace(href: string, routeOptions?: RouteOptions) {
|
|
249
367
|
this.#checkInitialized();
|
|
250
368
|
this.#instance.replace(href, routeOptions);
|
|
369
|
+
this.#rememberHistoryState("replace");
|
|
251
370
|
this.#postDevSyncNavigation("replace", href);
|
|
252
371
|
return undefined as never;
|
|
253
372
|
}
|
|
373
|
+
canGoBack() {
|
|
374
|
+
if (getEnv().side === "server") return false;
|
|
375
|
+
return this.#historyIdx > 0;
|
|
376
|
+
}
|
|
377
|
+
backOrFallback(fallbackHref = this.#indexPath, routeOptions?: RouteOptions) {
|
|
378
|
+
if (this.canGoBack()) {
|
|
379
|
+
this.back(routeOptions);
|
|
380
|
+
return undefined as never;
|
|
381
|
+
}
|
|
382
|
+
this.replace(fallbackHref, routeOptions);
|
|
383
|
+
return undefined as never;
|
|
384
|
+
}
|
|
254
385
|
back(routeOptions?: RouteOptions) {
|
|
255
386
|
if (getEnv().side === "server") throw new Error("back is only available in client side");
|
|
256
387
|
|
package/index.ts
CHANGED
|
@@ -33,10 +33,15 @@ export interface AkanMobileTargetAssets {
|
|
|
33
33
|
splash?: string;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
export interface
|
|
36
|
+
export interface AkanMobileTargetDeepLinks {
|
|
37
37
|
schemes?: string[];
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
domains?: string[];
|
|
39
|
+
ios?: {
|
|
40
|
+
teamId?: string;
|
|
41
|
+
};
|
|
42
|
+
android?: {
|
|
43
|
+
sha256CertFingerprints?: string[];
|
|
44
|
+
};
|
|
40
45
|
}
|
|
41
46
|
|
|
42
47
|
export interface AkanMobileTargetFiles {
|
|
@@ -54,13 +59,14 @@ export interface AkanCapacitorLikeConfig {
|
|
|
54
59
|
export interface AkanMobileTargetConfig extends AkanCapacitorLikeConfig {
|
|
55
60
|
name: string;
|
|
56
61
|
basePath?: string;
|
|
62
|
+
indexPath?: string;
|
|
57
63
|
appName: string;
|
|
58
64
|
appId: string;
|
|
59
65
|
version: string;
|
|
60
66
|
buildNum: number;
|
|
61
67
|
assets?: AkanMobileTargetAssets;
|
|
62
68
|
permissions?: MobilePermission[];
|
|
63
|
-
|
|
69
|
+
deepLinks?: AkanMobileTargetDeepLinks;
|
|
64
70
|
files?: AkanMobileTargetFiles;
|
|
65
71
|
}
|
|
66
72
|
|
|
@@ -83,6 +89,7 @@ export interface AppConfigResult {
|
|
|
83
89
|
i18n: AkanI18nConfig;
|
|
84
90
|
publicEnv: string[];
|
|
85
91
|
mobile: AkanMobileConfig;
|
|
92
|
+
secrets: string[];
|
|
86
93
|
}
|
|
87
94
|
|
|
88
95
|
export interface LibConfigResult {
|
package/package.json
CHANGED
|
@@ -49,7 +49,7 @@ export class LocaleWebProxy implements WebProxy {
|
|
|
49
49
|
(locale) => !pathname.startsWith(`/${locale}/`) && pathname !== `/${locale}`,
|
|
50
50
|
);
|
|
51
51
|
|
|
52
|
-
if (!isInternalProxyRequest(requestUrl) && pathnameIsMissingLocale) {
|
|
52
|
+
if (!isInternalProxyRequest(requestUrl) && !isWellKnownRequest(pathname) && pathnameIsMissingLocale) {
|
|
53
53
|
return Response.redirect(
|
|
54
54
|
new URL(`/${getLocale(request)}/${pathname.slice(1)}${targetUrl.search}`, getPublicRequestUrl(request)),
|
|
55
55
|
307,
|
|
@@ -86,3 +86,7 @@ function getPublicRequestUrl(request: Bun.BunRequest): URL {
|
|
|
86
86
|
function isInternalProxyRequest(requestUrl: URL): boolean {
|
|
87
87
|
return requestUrl.pathname === "/__rsc";
|
|
88
88
|
}
|
|
89
|
+
|
|
90
|
+
function isWellKnownRequest(pathname: string): boolean {
|
|
91
|
+
return pathname === "/.well-known" || pathname.startsWith("/.well-known/");
|
|
92
|
+
}
|
package/server/types.tsx
CHANGED
|
@@ -99,8 +99,17 @@ export type BaseBuildArtifact = {
|
|
|
99
99
|
branches: string[];
|
|
100
100
|
i18n: AkanI18nConfig;
|
|
101
101
|
imageConfig: AkanImageConfig;
|
|
102
|
+
deepLinkAssociations?: MobileDeepLinkAssociation[];
|
|
102
103
|
};
|
|
103
104
|
|
|
105
|
+
export interface MobileDeepLinkAssociation {
|
|
106
|
+
targetName: string;
|
|
107
|
+
appId: string;
|
|
108
|
+
domains: string[];
|
|
109
|
+
iosTeamId?: string;
|
|
110
|
+
androidSha256CertFingerprints?: string[];
|
|
111
|
+
}
|
|
112
|
+
|
|
104
113
|
export interface CssAsset {
|
|
105
114
|
cssUrl: string;
|
|
106
115
|
cssRelPath: string;
|
package/server/webRouter.ts
CHANGED
|
@@ -56,6 +56,8 @@ import type { BaseBuildArtifact, HttpRoutes, RenderState } from "./types";
|
|
|
56
56
|
const RESERVED_BASE_PATHS = new Set(["admin"]);
|
|
57
57
|
const CLIENT_CLOSED_REQUEST_STATUS = 499;
|
|
58
58
|
export const DEFAULT_HTML_RESULT_CACHE_MAX_BODY_BYTES = 2 * 1024 * 1024;
|
|
59
|
+
const APPLE_APP_SITE_ASSOCIATION_PATH = "/.well-known/apple-app-site-association";
|
|
60
|
+
const ANDROID_ASSET_LINKS_PATH = "/.well-known/assetlinks.json";
|
|
59
61
|
|
|
60
62
|
export function createRscRedirectResponse(
|
|
61
63
|
location: string,
|
|
@@ -459,6 +461,14 @@ export class WebRouter {
|
|
|
459
461
|
new Response(JSON.stringify(this.#routeCache.merged.clientManifest, null, 2), {
|
|
460
462
|
headers: { "Content-Type": "application/json; charset=utf-8" },
|
|
461
463
|
}),
|
|
464
|
+
[APPLE_APP_SITE_ASSOCIATION_PATH]: () =>
|
|
465
|
+
WebRouter.#deepLinkAssociationResponse(APPLE_APP_SITE_ASSOCIATION_PATH, this.#artifact, {
|
|
466
|
+
cacheControl: this.#prodMode ? "public, max-age=3600" : "no-store",
|
|
467
|
+
}) ?? new Response("Not Found", { status: 404 }),
|
|
468
|
+
[ANDROID_ASSET_LINKS_PATH]: () =>
|
|
469
|
+
WebRouter.#deepLinkAssociationResponse(ANDROID_ASSET_LINKS_PATH, this.#artifact, {
|
|
470
|
+
cacheControl: this.#prodMode ? "public, max-age=3600" : "no-store",
|
|
471
|
+
}) ?? new Response("Not Found", { status: 404 }),
|
|
462
472
|
"/*": async (req) => {
|
|
463
473
|
const url = new URL(req.url);
|
|
464
474
|
if (WebRouter.#isImageOptimizerPath(url.pathname)) {
|
|
@@ -1046,6 +1056,49 @@ export class WebRouter {
|
|
|
1046
1056
|
return new Response(file.stream(), { headers });
|
|
1047
1057
|
}
|
|
1048
1058
|
|
|
1059
|
+
static #deepLinkAssociationResponse(
|
|
1060
|
+
pathname: string,
|
|
1061
|
+
artifact: BaseBuildArtifact,
|
|
1062
|
+
{ cacheControl }: { cacheControl: string },
|
|
1063
|
+
) {
|
|
1064
|
+
if (pathname !== APPLE_APP_SITE_ASSOCIATION_PATH && pathname !== ANDROID_ASSET_LINKS_PATH) return null;
|
|
1065
|
+
const associations = artifact.deepLinkAssociations ?? [];
|
|
1066
|
+
if (pathname === APPLE_APP_SITE_ASSOCIATION_PATH) {
|
|
1067
|
+
const details = associations
|
|
1068
|
+
.filter((association) => association.domains.length > 0 && association.iosTeamId)
|
|
1069
|
+
.map((association) => ({
|
|
1070
|
+
appIDs: [`${association.iosTeamId}.${association.appId}`],
|
|
1071
|
+
components: [{ "/": "/*" }],
|
|
1072
|
+
}));
|
|
1073
|
+
if (details.length === 0) return null;
|
|
1074
|
+
return WebRouter.#jsonResponse({ applinks: { apps: [], details } }, cacheControl);
|
|
1075
|
+
}
|
|
1076
|
+
const assetLinks = associations
|
|
1077
|
+
.filter(
|
|
1078
|
+
(association) =>
|
|
1079
|
+
association.domains.length > 0 && (association.androidSha256CertFingerprints?.length ?? 0) > 0,
|
|
1080
|
+
)
|
|
1081
|
+
.map((association) => ({
|
|
1082
|
+
relation: ["delegate_permission/common.handle_all_urls"],
|
|
1083
|
+
target: {
|
|
1084
|
+
namespace: "android_app",
|
|
1085
|
+
package_name: association.appId,
|
|
1086
|
+
sha256_cert_fingerprints: association.androidSha256CertFingerprints,
|
|
1087
|
+
},
|
|
1088
|
+
}));
|
|
1089
|
+
if (assetLinks.length === 0) return null;
|
|
1090
|
+
return WebRouter.#jsonResponse(assetLinks, cacheControl);
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
static #jsonResponse(body: unknown, cacheControl: string) {
|
|
1094
|
+
return new Response(JSON.stringify(body, null, 2), {
|
|
1095
|
+
headers: {
|
|
1096
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
1097
|
+
"Cache-Control": cacheControl,
|
|
1098
|
+
},
|
|
1099
|
+
});
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1049
1102
|
static #bytesResponse(
|
|
1050
1103
|
_req: Request,
|
|
1051
1104
|
bytes: Uint8Array,
|
|
@@ -13,6 +13,9 @@ export type CapacitorAppModule = {
|
|
|
13
13
|
addListener: (eventName: string, listenerFunc: (...args: unknown[]) => void) => Promise<unknown> | unknown;
|
|
14
14
|
removeAllListeners: () => Promise<void> | void;
|
|
15
15
|
exitApp?: () => Promise<void> | void;
|
|
16
|
+
getLaunchUrl?: () => Promise<{
|
|
17
|
+
url?: string | null;
|
|
18
|
+
}>;
|
|
16
19
|
getInfo: () => Promise<{
|
|
17
20
|
id: string;
|
|
18
21
|
version: string;
|
package/types/client/router.d.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
export interface RouteOptions {
|
|
2
2
|
scrollToTop?: boolean;
|
|
3
3
|
}
|
|
4
|
+
export interface RouteManifest {
|
|
5
|
+
routes: string[];
|
|
6
|
+
}
|
|
7
|
+
export interface DeepLinkOptions extends RouteOptions {
|
|
8
|
+
resetStack?: boolean;
|
|
9
|
+
}
|
|
4
10
|
export interface RouterInstance {
|
|
5
11
|
push: (href: string, routeOptions?: RouteOptions) => void;
|
|
6
12
|
replace: (href: string, routeOptions?: RouteOptions) => void;
|
|
@@ -10,6 +16,8 @@ export interface RouterInstance {
|
|
|
10
16
|
interface RouterOptions {
|
|
11
17
|
prefix?: string;
|
|
12
18
|
lang?: string;
|
|
19
|
+
routeManifest?: RouteManifest | string[];
|
|
20
|
+
indexPath?: string;
|
|
13
21
|
}
|
|
14
22
|
interface SsrServerRouterOption extends RouterOptions {
|
|
15
23
|
type: "ssr";
|
|
@@ -41,6 +49,7 @@ export declare class AkanNotFoundError extends Error {
|
|
|
41
49
|
readonly digest = "AKAN_NOT_FOUND";
|
|
42
50
|
constructor();
|
|
43
51
|
}
|
|
52
|
+
export declare const normalizeDeepLinkHref: (href: string, origin?: string) => string;
|
|
44
53
|
declare global {
|
|
45
54
|
var __AKAN_ROUTER__: Router | undefined;
|
|
46
55
|
var __AKAN_DEV_SYNC_NAVIGATION__: ((href: string, kind: "push" | "replace" | "back" | "pop") => void) | undefined;
|
|
@@ -57,8 +66,12 @@ declare class Router {
|
|
|
57
66
|
#private;
|
|
58
67
|
isInitialized: boolean;
|
|
59
68
|
init(options: SsrClientRouterOption | SsrServerRouterOption | CSRClientRouterOption): void;
|
|
69
|
+
resolveDeepLinkStack(href: string): string[];
|
|
70
|
+
enterDeepLink(href: string, options?: DeepLinkOptions): boolean;
|
|
60
71
|
push(href: string, routeOptions?: RouteOptions): never;
|
|
61
72
|
replace(href: string, routeOptions?: RouteOptions): never;
|
|
73
|
+
canGoBack(): boolean;
|
|
74
|
+
backOrFallback(fallbackHref?: string, routeOptions?: RouteOptions): never;
|
|
62
75
|
back(routeOptions?: RouteOptions): never;
|
|
63
76
|
refresh(): never;
|
|
64
77
|
redirect(href: string, options?: RedirectOptions): never;
|
package/types/index.d.ts
CHANGED
|
@@ -32,10 +32,15 @@ export interface AkanMobileTargetAssets {
|
|
|
32
32
|
icon?: string;
|
|
33
33
|
splash?: string;
|
|
34
34
|
}
|
|
35
|
-
export interface
|
|
35
|
+
export interface AkanMobileTargetDeepLinks {
|
|
36
36
|
schemes?: string[];
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
domains?: string[];
|
|
38
|
+
ios?: {
|
|
39
|
+
teamId?: string;
|
|
40
|
+
};
|
|
41
|
+
android?: {
|
|
42
|
+
sha256CertFingerprints?: string[];
|
|
43
|
+
};
|
|
39
44
|
}
|
|
40
45
|
export interface AkanMobileTargetFiles {
|
|
41
46
|
ios?: Record<string, string>;
|
|
@@ -50,13 +55,14 @@ export interface AkanCapacitorLikeConfig {
|
|
|
50
55
|
export interface AkanMobileTargetConfig extends AkanCapacitorLikeConfig {
|
|
51
56
|
name: string;
|
|
52
57
|
basePath?: string;
|
|
58
|
+
indexPath?: string;
|
|
53
59
|
appName: string;
|
|
54
60
|
appId: string;
|
|
55
61
|
version: string;
|
|
56
62
|
buildNum: number;
|
|
57
63
|
assets?: AkanMobileTargetAssets;
|
|
58
64
|
permissions?: MobilePermission[];
|
|
59
|
-
|
|
65
|
+
deepLinks?: AkanMobileTargetDeepLinks;
|
|
60
66
|
files?: AkanMobileTargetFiles;
|
|
61
67
|
}
|
|
62
68
|
export interface AkanMobileConfig extends AkanCapacitorLikeConfig {
|
|
@@ -77,6 +83,7 @@ export interface AppConfigResult {
|
|
|
77
83
|
i18n: AkanI18nConfig;
|
|
78
84
|
publicEnv: string[];
|
|
79
85
|
mobile: AkanMobileConfig;
|
|
86
|
+
secrets: string[];
|
|
80
87
|
}
|
|
81
88
|
export interface LibConfigResult {
|
|
82
89
|
externalLibs: string[];
|
package/types/server/types.d.ts
CHANGED
|
@@ -59,7 +59,15 @@ export type BaseBuildArtifact = {
|
|
|
59
59
|
branches: string[];
|
|
60
60
|
i18n: AkanI18nConfig;
|
|
61
61
|
imageConfig: AkanImageConfig;
|
|
62
|
+
deepLinkAssociations?: MobileDeepLinkAssociation[];
|
|
62
63
|
};
|
|
64
|
+
export interface MobileDeepLinkAssociation {
|
|
65
|
+
targetName: string;
|
|
66
|
+
appId: string;
|
|
67
|
+
domains: string[];
|
|
68
|
+
iosTeamId?: string;
|
|
69
|
+
androidSha256CertFingerprints?: string[];
|
|
70
|
+
}
|
|
63
71
|
export interface CssAsset {
|
|
64
72
|
cssUrl: string;
|
|
65
73
|
cssRelPath: string;
|
|
@@ -3,6 +3,9 @@ import { type Location, type PageState, type TransitionStyle } from "akanjs/clie
|
|
|
3
3
|
import type { AkanTheme } from "akanjs/fetch";
|
|
4
4
|
import type { SerializedSignal } from "akanjs/signal";
|
|
5
5
|
import { type HTMLAttributes, type ReactNode, type RefObject } from "react";
|
|
6
|
+
declare global {
|
|
7
|
+
var __AKAN_GET_SYNC_ROUTE_HREF__: ((href: string) => string) | undefined;
|
|
8
|
+
}
|
|
6
9
|
export declare const Client: {
|
|
7
10
|
(): import("react/jsx-runtime").JSX.Element;
|
|
8
11
|
Wrapper: ({ children, theme, lang, path, dictionary, signals, reconnect, }: ClientWrapperProps) => import("react/jsx-runtime").JSX.Element;
|
package/ui/Dialog/Modal.tsx
CHANGED
|
@@ -5,7 +5,7 @@ import { animated } from "akanjs/ui";
|
|
|
5
5
|
import { type ReactNode, useCallback, useContext, useEffect, useId, useRef, useState } from "react";
|
|
6
6
|
import { createPortal } from "react-dom";
|
|
7
7
|
import { BiX } from "react-icons/bi";
|
|
8
|
-
import { config, useSpring } from "react-spring";
|
|
8
|
+
import { config, useReducedMotion, useSpring } from "react-spring";
|
|
9
9
|
|
|
10
10
|
import { DialogContext } from "./context";
|
|
11
11
|
|
|
@@ -30,7 +30,10 @@ export const Modal = ({ className, bodyClassName, confirmClose, children, onCanc
|
|
|
30
30
|
const { l } = usePage();
|
|
31
31
|
const ref = useRef<HTMLDivElement>(null);
|
|
32
32
|
const closeTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
33
|
+
const closeFallbackRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
33
34
|
const closingRef = useRef(false);
|
|
35
|
+
|
|
36
|
+
useReducedMotion();
|
|
34
37
|
const focusedElementRef = useRef<HTMLElement | null>(null);
|
|
35
38
|
const titleId = useId();
|
|
36
39
|
const contentId = useId();
|
|
@@ -42,6 +45,8 @@ export const Modal = ({ className, bodyClassName, confirmClose, children, onCanc
|
|
|
42
45
|
const openModal = useCallback(
|
|
43
46
|
async ({ canceled }: { canceled?: boolean } = {}) => {
|
|
44
47
|
closingRef.current = false;
|
|
48
|
+
if (closeFallbackRef.current) clearTimeout(closeFallbackRef.current);
|
|
49
|
+
closeFallbackRef.current = null;
|
|
45
50
|
setIsMounted(true);
|
|
46
51
|
if (closeTimerRef.current) clearTimeout(closeTimerRef.current);
|
|
47
52
|
closeTimerRef.current = setTimeout(() => {
|
|
@@ -52,6 +57,19 @@ export const Modal = ({ className, bodyClassName, confirmClose, children, onCanc
|
|
|
52
57
|
[api],
|
|
53
58
|
);
|
|
54
59
|
|
|
60
|
+
const finalizeClose = useCallback(
|
|
61
|
+
({ notifyCancel }: { notifyCancel: boolean }) => {
|
|
62
|
+
if (!closingRef.current) return;
|
|
63
|
+
if (closeFallbackRef.current) clearTimeout(closeFallbackRef.current);
|
|
64
|
+
closeFallbackRef.current = null;
|
|
65
|
+
setIsMounted(false);
|
|
66
|
+
setOpen(false);
|
|
67
|
+
if (notifyCancel) onCancel?.();
|
|
68
|
+
closingRef.current = false;
|
|
69
|
+
},
|
|
70
|
+
[onCancel, setOpen],
|
|
71
|
+
);
|
|
72
|
+
|
|
55
73
|
const closeModal = useCallback(
|
|
56
74
|
async ({
|
|
57
75
|
velocity = 0,
|
|
@@ -72,13 +90,19 @@ export const Modal = ({ className, bodyClassName, confirmClose, children, onCanc
|
|
|
72
90
|
closeTimerRef.current = setTimeout(() => {
|
|
73
91
|
setShowBackground(false);
|
|
74
92
|
}, 100);
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
93
|
+
|
|
94
|
+
if (closeFallbackRef.current) clearTimeout(closeFallbackRef.current);
|
|
95
|
+
closeFallbackRef.current = setTimeout(() => {
|
|
96
|
+
finalizeClose({ notifyCancel });
|
|
97
|
+
}, 400);
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
await Promise.all(api.start({ translate: 1, immediate: false, config: { ...config.stiff, velocity } }));
|
|
101
|
+
} finally {
|
|
102
|
+
finalizeClose({ notifyCancel });
|
|
103
|
+
}
|
|
80
104
|
},
|
|
81
|
-
[api, l,
|
|
105
|
+
[api, l, finalizeClose],
|
|
82
106
|
);
|
|
83
107
|
|
|
84
108
|
const requestClose = useCallback(
|
|
@@ -177,6 +201,7 @@ export const Modal = ({ className, bodyClassName, confirmClose, children, onCanc
|
|
|
177
201
|
useEffect(() => {
|
|
178
202
|
return () => {
|
|
179
203
|
if (closeTimerRef.current) clearTimeout(closeTimerRef.current);
|
|
204
|
+
if (closeFallbackRef.current) clearTimeout(closeFallbackRef.current);
|
|
180
205
|
};
|
|
181
206
|
}, []);
|
|
182
207
|
|
package/ui/System/CSR.tsx
CHANGED
|
@@ -133,7 +133,15 @@ const CSRWrapper = ({
|
|
|
133
133
|
document.documentElement.classList.add("akan-mobile-document");
|
|
134
134
|
document.body.classList.add("akan-mobile-document");
|
|
135
135
|
}
|
|
136
|
-
if (!router.isInitialized)
|
|
136
|
+
if (!router.isInitialized)
|
|
137
|
+
router.init({
|
|
138
|
+
type: "csr",
|
|
139
|
+
lang,
|
|
140
|
+
prefix,
|
|
141
|
+
router: reactRouter,
|
|
142
|
+
routeManifest: pathRoutes.map((pathRoute) => pathRoute.path),
|
|
143
|
+
indexPath: window.__AKAN_MOBILE_TARGET__?.indexPath,
|
|
144
|
+
});
|
|
137
145
|
st.do.setCsrLoaded(true);
|
|
138
146
|
const onVisibilityChange = () => debugFrame("document.visibility", { visibilityState: document.visibilityState });
|
|
139
147
|
const onPageHide = (event: PageTransitionEvent) => debugFrame("window.pagehide", { persisted: event.persisted });
|
package/ui/System/Client.tsx
CHANGED
|
@@ -42,6 +42,10 @@ import { Messages } from "./Messages";
|
|
|
42
42
|
import { Reconnect } from "./Reconnect";
|
|
43
43
|
import { getFrameCssVars } from "./frameCssVars";
|
|
44
44
|
|
|
45
|
+
declare global {
|
|
46
|
+
var __AKAN_GET_SYNC_ROUTE_HREF__: ((href: string) => string) | undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
45
49
|
export const Client = () => {
|
|
46
50
|
return <></>;
|
|
47
51
|
};
|
package/webkit/bootCsr.tsx
CHANGED
|
@@ -35,7 +35,7 @@ type CsrRouteModuleEntry = CsrRouteModuleLoader | { loader: CsrRouteModuleLoader
|
|
|
35
35
|
|
|
36
36
|
declare global {
|
|
37
37
|
interface Window {
|
|
38
|
-
__AKAN_MOBILE_TARGET__?: { name: string; basePath?: string };
|
|
38
|
+
__AKAN_MOBILE_TARGET__?: { name: string; basePath?: string; indexPath?: string };
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
|
|
@@ -274,7 +274,8 @@ function initializeMobileTargetFromSearch() {
|
|
|
274
274
|
if (!name) return;
|
|
275
275
|
|
|
276
276
|
const basePath = params.get("akanMobileBasePath")?.replace(/^\/+|\/+$/g, "") ?? "";
|
|
277
|
-
|
|
277
|
+
const indexPath = params.get("akanMobileIndexPath") ?? undefined;
|
|
278
|
+
window.__AKAN_MOBILE_TARGET__ = { name, basePath, ...(indexPath ? { indexPath } : {}) };
|
|
278
279
|
}
|
|
279
280
|
|
|
280
281
|
function validateRouteModuleExports(key: string, mod: RouteModule) {
|
package/webkit/useCsrValues.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
getPathInfo,
|
|
11
11
|
type LocationState,
|
|
12
12
|
type NavigationIntent,
|
|
13
|
+
normalizeDeepLinkHref,
|
|
13
14
|
type PageState,
|
|
14
15
|
type PathRoute,
|
|
15
16
|
type RouteGuide,
|
|
@@ -61,7 +62,7 @@ const STACK_VELOCITY_DISMISS_THRESHOLD = 0.45;
|
|
|
61
62
|
const STACK_SETTLE_MIN_DURATION = 90;
|
|
62
63
|
const STACK_SETTLE_MAX_DURATION = 260;
|
|
63
64
|
const ANDROID_SCALE_TRANSITION_DURATION = 220;
|
|
64
|
-
const CSR_RUNTIME_SEARCH_PARAMS = ["csr", "akanMobileTarget", "akanMobileBasePath"] as const;
|
|
65
|
+
const CSR_RUNTIME_SEARCH_PARAMS = ["csr", "akanMobileTarget", "akanMobileBasePath", "akanMobileIndexPath"] as const;
|
|
65
66
|
|
|
66
67
|
const clamp = (value: number, min: number, max: number) => Math.min(max, Math.max(min, value));
|
|
67
68
|
|
|
@@ -1240,6 +1241,8 @@ export const useCsrValues = (rootRouteGuide: RouteGuide, pathRoutes: PathRoute[]
|
|
|
1240
1241
|
keyboardVisible: keyboardFrame.visible,
|
|
1241
1242
|
router,
|
|
1242
1243
|
});
|
|
1244
|
+
const handledDeepLinkRef = useRef<{ href: string; handledAt: number; resetStack: boolean } | null>(null);
|
|
1245
|
+
const didResetDeepLinkStackRef = useRef(false);
|
|
1243
1246
|
|
|
1244
1247
|
useEffect(() => {
|
|
1245
1248
|
if (pageContentRef.current) pageContentRef.current.scrollTop = getScrollTop(location);
|
|
@@ -1256,6 +1259,74 @@ export const useCsrValues = (rootRouteGuide: RouteGuide, pathRoutes: PathRoute[]
|
|
|
1256
1259
|
};
|
|
1257
1260
|
}, [keyboardFrame.height, keyboardFrame.visible, resolvedLocation.pathRoute.path, router]);
|
|
1258
1261
|
|
|
1262
|
+
useEffect(() => {
|
|
1263
|
+
const isMobileTarget = Boolean(window.__AKAN_MOBILE_TARGET__);
|
|
1264
|
+
if (Device.getDevice().info.platform === "web" && !isMobileTarget) return;
|
|
1265
|
+
let removeListener: (() => void) | undefined;
|
|
1266
|
+
let disposed = false;
|
|
1267
|
+
const mountedAt = Date.now();
|
|
1268
|
+
|
|
1269
|
+
const enterDeepLinkWhenReady = (href: string, resetStack: boolean, attempt = 0) => {
|
|
1270
|
+
if (!clientRouter.isInitialized) {
|
|
1271
|
+
if (attempt < 40) window.setTimeout(() => enterDeepLinkWhenReady(href, resetStack, attempt + 1), 50);
|
|
1272
|
+
else debugFrame("native.deepLink.skipped", { href, reason: "router-not-ready" });
|
|
1273
|
+
return;
|
|
1274
|
+
}
|
|
1275
|
+
clientRouter.enterDeepLink(href, { resetStack, scrollToTop: true });
|
|
1276
|
+
};
|
|
1277
|
+
|
|
1278
|
+
const handleDeepLink = (url: string | null | undefined, resetStack: boolean) => {
|
|
1279
|
+
if (!url) return;
|
|
1280
|
+
const href = normalizeDeepLinkHref(url);
|
|
1281
|
+
const now = Date.now();
|
|
1282
|
+
const lastHandled = handledDeepLinkRef.current;
|
|
1283
|
+
const shouldResetStack = resetStack || (!lastHandled && now - mountedAt < 5000);
|
|
1284
|
+
if (lastHandled?.href === href && now - lastHandled.handledAt < 1000 && (!shouldResetStack || lastHandled.resetStack))
|
|
1285
|
+
return;
|
|
1286
|
+
handledDeepLinkRef.current = { href, handledAt: now, resetStack: shouldResetStack };
|
|
1287
|
+
debugFrame("native.deepLink", {
|
|
1288
|
+
href,
|
|
1289
|
+
resetStack: shouldResetStack,
|
|
1290
|
+
sourceResetStack: resetStack,
|
|
1291
|
+
historyIdx: history.current.idx,
|
|
1292
|
+
mountedForMs: now - mountedAt,
|
|
1293
|
+
routerReady: clientRouter.isInitialized,
|
|
1294
|
+
});
|
|
1295
|
+
if (shouldResetStack) didResetDeepLinkStackRef.current = true;
|
|
1296
|
+
enterDeepLinkWhenReady(href, shouldResetStack);
|
|
1297
|
+
};
|
|
1298
|
+
|
|
1299
|
+
void loadCapacitorApp()
|
|
1300
|
+
.then(({ App }) => {
|
|
1301
|
+
debugFrame("native.deepLink.listener", { platform: Device.getDevice().info.platform, isMobileTarget });
|
|
1302
|
+
const listener = App.addListener("appUrlOpen", (event: unknown) => {
|
|
1303
|
+
handleDeepLink((event as { url?: string | null } | undefined)?.url, false);
|
|
1304
|
+
});
|
|
1305
|
+
|
|
1306
|
+
void Promise.resolve(listener).then((handle) => {
|
|
1307
|
+
const remove =
|
|
1308
|
+
typeof (handle as { remove?: unknown } | undefined)?.remove === "function"
|
|
1309
|
+
? () => void (handle as { remove: () => Promise<void> | void }).remove()
|
|
1310
|
+
: undefined;
|
|
1311
|
+
if (disposed) remove?.();
|
|
1312
|
+
else removeListener = remove;
|
|
1313
|
+
});
|
|
1314
|
+
|
|
1315
|
+
void App.getLaunchUrl?.()
|
|
1316
|
+
.then((launch) => {
|
|
1317
|
+
debugFrame("native.deepLink.launchUrl", { url: launch?.url ?? null });
|
|
1318
|
+
handleDeepLink(launch?.url, true);
|
|
1319
|
+
})
|
|
1320
|
+
.catch((error) => debugFrame("native.deepLink.launchUrlError", { error: String(error) }));
|
|
1321
|
+
})
|
|
1322
|
+
.catch((error) => debugFrame("native.deepLink.listenerError", { error: String(error) }));
|
|
1323
|
+
|
|
1324
|
+
return () => {
|
|
1325
|
+
disposed = true;
|
|
1326
|
+
removeListener?.();
|
|
1327
|
+
};
|
|
1328
|
+
}, []);
|
|
1329
|
+
|
|
1259
1330
|
useEffect(() => {
|
|
1260
1331
|
if (Device.getDevice().info.platform === "web") return;
|
|
1261
1332
|
let removeListener: (() => void) | undefined;
|
|
@@ -1277,6 +1348,15 @@ export const useCsrValues = (rootRouteGuide: RouteGuide, pathRoutes: PathRoute[]
|
|
|
1277
1348
|
nativeBackState.router.back();
|
|
1278
1349
|
return;
|
|
1279
1350
|
}
|
|
1351
|
+
const fallbackPath = window.__AKAN_MOBILE_TARGET__?.indexPath ?? "/";
|
|
1352
|
+
if (didResetDeepLinkStackRef.current) {
|
|
1353
|
+
void App.exitApp?.();
|
|
1354
|
+
return;
|
|
1355
|
+
}
|
|
1356
|
+
if (nativeBackState.path !== fallbackPath) {
|
|
1357
|
+
clientRouter.backOrFallback(fallbackPath, { scrollToTop: false });
|
|
1358
|
+
return;
|
|
1359
|
+
}
|
|
1280
1360
|
void App.exitApp?.();
|
|
1281
1361
|
});
|
|
1282
1362
|
|