akanjs 2.1.0-rc.0 → 2.1.0-rc.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/capacitor.base.config.ts +88 -0
  2. package/client/capacitor.ts +182 -24
  3. package/client/cookie.ts +14 -5
  4. package/client/device.ts +86 -10
  5. package/client/index.ts +0 -1
  6. package/client/storage.ts +38 -14
  7. package/document/filterMeta.ts +5 -0
  8. package/fetch/client/fetchClient.ts +17 -3
  9. package/package.json +15 -13
  10. package/server/di/predefinedAdaptor.ts +2 -2
  11. package/server/resolver/database.resolver.ts +8 -5
  12. package/server/resolver/service.resolver.ts +6 -3
  13. package/service/predefinedAdaptor/compress.adaptor.ts +58 -17
  14. package/service/predefinedAdaptor/database.adaptor.ts +2 -1
  15. package/service/predefinedAdaptor/queue.adaptor.ts +35 -16
  16. package/store/action.ts +38 -15
  17. package/store/state.ts +4 -13
  18. package/types/capacitor.base.config.d.ts +3 -0
  19. package/types/client/capacitor.d.ts +224 -24
  20. package/types/client/device.d.ts +9 -5
  21. package/types/client/index.d.ts +0 -1
  22. package/types/dictionary/base.dictionary.d.ts +1 -1
  23. package/types/dictionary/dictionary.d.ts +8 -8
  24. package/types/document/filterMeta.d.ts +1 -0
  25. package/types/server/di/predefinedAdaptor.d.ts +2 -2
  26. package/types/service/predefinedAdaptor/compress.adaptor.d.ts +8 -0
  27. package/types/service/predefinedAdaptor/queue.adaptor.d.ts +8 -5
  28. package/types/ui/Copy.d.ts +1 -1
  29. package/types/webkit/useCamera.d.ts +14 -3
  30. package/types/webkit/useContact.d.ts +6 -2
  31. package/types/webkit/useGeoLocation.d.ts +1 -1
  32. package/ui/Copy.tsx +36 -7
  33. package/ui/Data/ListContainer.tsx +14 -3
  34. package/ui/DatePicker.tsx +14 -7
  35. package/ui/Link/CsrLink.tsx +5 -2
  36. package/ui/More.tsx +2 -3
  37. package/ui/Refresh.tsx +5 -1
  38. package/webkit/useCamera.tsx +8 -4
  39. package/webkit/useCodepush.tsx +1 -1
  40. package/webkit/useContact.tsx +6 -2
  41. package/webkit/useCsrValues.ts +2 -1
  42. package/webkit/useGeoLocation.tsx +1 -1
  43. package/webkit/usePurchase.tsx +1 -1
  44. package/webkit/usePushNoti.tsx +1 -1
@@ -0,0 +1,88 @@
1
+ import os from "node:os";
2
+ import type { CapacitorConfig } from "@capacitor/cli";
3
+ import type { AkanMobileTargetConfig, AppScanResult } from "akanjs";
4
+
5
+ const getLocalIP = () => {
6
+ const interfaces = os.networkInterfaces();
7
+ for (const interfaceName in interfaces) {
8
+ const iface = interfaces[interfaceName];
9
+ if (!iface) continue;
10
+ for (const alias of iface) {
11
+ if (alias.family === "IPv4" && !alias.internal) return alias.address;
12
+ }
13
+ }
14
+ return "127.0.0.1";
15
+ };
16
+
17
+ const normalizeBasePath = (basePath: string | undefined) => basePath?.replace(/^\/+|\/+$/g, "");
18
+
19
+ const routeBasePaths = (appInfo: AppScanResult) =>
20
+ new Set(
21
+ appInfo.routes
22
+ .map((route) => route.replace(/^\.\//, "").split("/")[0])
23
+ .filter((segment): segment is string => !!segment && !segment.startsWith("_") && !segment.startsWith("(")),
24
+ );
25
+
26
+ const resolveTarget = (appInfo: AppScanResult, targetName = process.env.AKAN_MOBILE_TARGET) => {
27
+ const targets = appInfo.akanConfig.mobile.targets;
28
+ if (!targets || Object.keys(targets).length === 0) throw new Error("Akan mobile target metadata is missing.");
29
+ if (targetName) {
30
+ const target = targets[targetName];
31
+ if (!target) {
32
+ const basePath = normalizeBasePath(targetName);
33
+ const [template] = Object.values(targets);
34
+ if (basePath && template && routeBasePaths(appInfo).has(basePath))
35
+ return { ...template, name: basePath, basePath };
36
+ throw new Error(`Akan mobile target '${targetName}' was not found.`);
37
+ }
38
+ return target;
39
+ }
40
+ const entries = Object.entries(targets);
41
+ if (entries.length !== 1) throw new Error("AKAN_MOBILE_TARGET is required when multiple mobile targets exist.");
42
+ return entries[0]?.[1] as AkanMobileTargetConfig;
43
+ };
44
+
45
+ const localCsrUrl = (ip: string, target: AkanMobileTargetConfig) => {
46
+ const basePath = normalizeBasePath(target.basePath);
47
+ const port = process.env.AKAN_PUBLIC_CLIENT_PORT ?? process.env.PORT ?? "8282";
48
+ return `http://${ip}:${port}/${basePath ? `${basePath}` : ""}?csr=true`;
49
+ };
50
+
51
+ export const withBase = (
52
+ configImp: (config: CapacitorConfig, target: AkanMobileTargetConfig) => CapacitorConfig = (config) => config,
53
+ appData?: AppScanResult,
54
+ targetName?: string,
55
+ ) => {
56
+ const ip = getLocalIP();
57
+ const appInfo = appData;
58
+ if (!appInfo) throw new Error("withBase requires apps/<app>/akan.app.json metadata.");
59
+ const target = resolveTarget(appInfo, targetName);
60
+ const baseConfig: CapacitorConfig = {
61
+ ...target,
62
+ appId: target.appId,
63
+ appName: target.appName,
64
+ webDir: "dist",
65
+ server:
66
+ process.env.APP_OPERATION_MODE !== "release"
67
+ ? {
68
+ androidScheme: "http",
69
+ url: localCsrUrl(ip, target),
70
+ cleartext: true,
71
+ allowNavigation: [ip, "localhost"],
72
+ }
73
+ : {
74
+ allowNavigation: ["*"],
75
+ },
76
+ plugins: {
77
+ CapacitorCookies: { enabled: true },
78
+ ...target.plugins,
79
+ },
80
+ android: {
81
+ ...target.android,
82
+ },
83
+ ios: {
84
+ ...target.ios,
85
+ },
86
+ };
87
+ return configImp(baseConfig, target);
88
+ };
@@ -1,16 +1,145 @@
1
+ export type CapacitorDeviceInfo = {
2
+ platform: string;
3
+ isVirtual: boolean;
4
+ osVersion: string;
5
+ [key: string]: unknown;
6
+ };
7
+
8
+ export type CapacitorKeyboardInfo = {
9
+ keyboardHeight: number;
10
+ };
11
+
12
+ export type CapacitorPermissionState = "prompt" | "prompt-with-rationale" | "granted" | "denied" | string;
13
+
14
+ export type CapacitorAppModule = {
15
+ App: {
16
+ addListener: (eventName: string, listenerFunc: (...args: unknown[]) => void) => Promise<unknown> | unknown;
17
+ removeAllListeners: () => Promise<void> | void;
18
+ getInfo: () => Promise<{ id: string; version: string; build: string; [key: string]: unknown }>;
19
+ };
20
+ };
21
+
22
+ export type CapacitorBrowserModule = {
23
+ Browser: {
24
+ open: (options: { url: string; presentationStyle?: string }) => Promise<void> | void;
25
+ };
26
+ };
27
+
28
+ export type CapacitorCameraModule = {
29
+ Camera: {
30
+ checkPermissions: () => Promise<{ camera: CapacitorPermissionState; photos: CapacitorPermissionState }>;
31
+ requestPermissions: () => Promise<{ camera: CapacitorPermissionState; photos: CapacitorPermissionState }>;
32
+ getPhoto: (options: Record<string, unknown>) => Promise<{ dataUrl?: string; [key: string]: unknown }>;
33
+ pickImages: (options: Record<string, unknown>) => Promise<{ photos: unknown[]; [key: string]: unknown }>;
34
+ };
35
+ CameraResultType: { DataUrl: string };
36
+ CameraSource: { Prompt: string; Camera: string; Photos: string };
37
+ };
38
+
39
+ export type CapacitorContactsModule = {
40
+ Contacts: {
41
+ checkPermissions: () => Promise<{ contacts: CapacitorPermissionState }>;
42
+ requestPermissions: () => Promise<{ contacts: CapacitorPermissionState }>;
43
+ getContacts: (options: Record<string, unknown>) => Promise<{ contacts: unknown[] }>;
44
+ };
45
+ };
46
+
47
+ export type CapacitorCoreModule = {
48
+ CapacitorCookies: {
49
+ setCookie: (options: { key: string; value: string; path?: string }) => Promise<void> | void;
50
+ };
51
+ };
52
+
53
+ export type CapacitorDeviceModule = {
54
+ Device: {
55
+ getInfo: () => Promise<CapacitorDeviceInfo>;
56
+ getLanguageCode: () => Promise<{ value: string }>;
57
+ };
58
+ };
59
+
60
+ export type CapacitorFcmModule = {
61
+ FCM: {
62
+ setAutoInit: (options: { enabled: boolean }) => Promise<void> | void;
63
+ getToken: () => Promise<{ token: string }>;
64
+ };
65
+ };
66
+
67
+ export type CapacitorGeolocationModule = {
68
+ Geolocation: {
69
+ requestPermissions: () => Promise<{ location: string; coarseLocation: string; [key: string]: string }>;
70
+ getCurrentPosition: () => Promise<unknown>;
71
+ };
72
+ };
73
+
74
+ export type CapacitorHapticsModule = {
75
+ ImpactStyle: { Light: string; Medium: string; Heavy: string };
76
+ Haptics: {
77
+ vibrate: (options: { duration: number }) => Promise<void> | void;
78
+ impact: (options: { style: string }) => Promise<void> | void;
79
+ selectionStart: () => Promise<void> | void;
80
+ selectionChanged: () => Promise<void> | void;
81
+ selectionEnd: () => Promise<void> | void;
82
+ };
83
+ };
84
+
85
+ export type CapacitorKeyboardModule = {
86
+ Keyboard: {
87
+ show: () => Promise<void> | void;
88
+ hide: () => Promise<void> | void;
89
+ addListener: (eventName: string, listenerFunc: (info: CapacitorKeyboardInfo) => void) => Promise<unknown> | unknown;
90
+ removeAllListeners: () => Promise<void> | void;
91
+ };
92
+ };
93
+
94
+ export type CapacitorPreferencesModule = {
95
+ Preferences: {
96
+ get: (options: { key: string }) => Promise<{ value: string | null }>;
97
+ set: (options: { key: string; value: string }) => Promise<void> | void;
98
+ remove: (options: { key: string }) => Promise<void> | void;
99
+ };
100
+ };
101
+
102
+ export type CapacitorPushNotificationsModule = {
103
+ PushNotifications: {
104
+ requestPermissions: () => Promise<{ receive: "granted" | "denied" | string }>;
105
+ checkPermissions: () => Promise<{ receive: "granted" | "denied" | string }>;
106
+ register: () => Promise<void> | void;
107
+ };
108
+ };
109
+
110
+ export type CapacitorSafeAreaModule = {
111
+ SafeArea: {
112
+ getSafeAreaInsets: () => Promise<{ insets: { top: number; bottom: number } }>;
113
+ };
114
+ };
115
+
116
+ export type CapacitorUpdaterModule = {
117
+ CapacitorUpdater: {
118
+ notifyAppReady: () => Promise<void> | void;
119
+ getPluginVersion: () => Promise<{ version: string }>;
120
+ getDeviceId: () => Promise<{ deviceId: string }>;
121
+ current: () => Promise<{ bundle: { version: string }; native: string }>;
122
+ getBuiltinVersion: () => Promise<{ version: string }>;
123
+ download: (options: { url: string; version: string }) => Promise<unknown>;
124
+ set: (bundle: unknown) => Promise<void> | void;
125
+ };
126
+ };
127
+
1
128
  type CapacitorModuleMap = {
2
- app: typeof import("@capacitor/app");
3
- browser: typeof import("@capacitor/browser");
4
- camera: typeof import("@capacitor/camera");
5
- contacts: typeof import("@capacitor-community/contacts");
6
- device: typeof import("@capacitor/device");
7
- fcm: typeof import("@capacitor-community/fcm");
8
- geolocation: typeof import("@capacitor/geolocation");
9
- haptics: typeof import("@capacitor/haptics");
10
- keyboard: typeof import("@capacitor/keyboard");
11
- pushNotifications: typeof import("@capacitor/push-notifications");
12
- safeArea: typeof import("capacitor-plugin-safe-area");
13
- updater: typeof import("@capgo/capacitor-updater");
129
+ app: CapacitorAppModule;
130
+ browser: CapacitorBrowserModule;
131
+ camera: CapacitorCameraModule;
132
+ contacts: CapacitorContactsModule;
133
+ core: CapacitorCoreModule;
134
+ device: CapacitorDeviceModule;
135
+ fcm: CapacitorFcmModule;
136
+ geolocation: CapacitorGeolocationModule;
137
+ haptics: CapacitorHapticsModule;
138
+ keyboard: CapacitorKeyboardModule;
139
+ preferences: CapacitorPreferencesModule;
140
+ pushNotifications: CapacitorPushNotificationsModule;
141
+ safeArea: CapacitorSafeAreaModule;
142
+ updater: CapacitorUpdaterModule;
14
143
  };
15
144
 
16
145
  type CapacitorImportCache = Partial<{
@@ -40,29 +169,58 @@ const loadCapacitorModule = <K extends keyof CapacitorModuleMap>(
40
169
  return loaded;
41
170
  };
42
171
 
43
- export const loadCapacitorApp = () => loadCapacitorModule("app", () => import("@capacitor/app"));
172
+ const importNativeModule = <T>(specifier: string) => import(specifier) as Promise<T>;
173
+
174
+ const capacitorPackage = (name: string) => `@capacitor/${name}`;
44
175
 
45
- export const loadCapacitorBrowser = () => loadCapacitorModule("browser", () => import("@capacitor/browser"));
176
+ const capacitorCommunityPackage = (name: string) => `@capacitor-community/${name}`;
46
177
 
47
- export const loadCapacitorCamera = () => loadCapacitorModule("camera", () => import("@capacitor/camera"));
178
+ export const loadCapacitorApp = () =>
179
+ loadCapacitorModule("app", () => importNativeModule<CapacitorAppModule>(capacitorPackage("app")));
180
+
181
+ export const loadCapacitorBrowser = () =>
182
+ loadCapacitorModule("browser", () => importNativeModule<CapacitorBrowserModule>(capacitorPackage("browser")));
183
+
184
+ export const loadCapacitorCamera = () =>
185
+ loadCapacitorModule("camera", () => importNativeModule<CapacitorCameraModule>(capacitorPackage("camera")));
48
186
 
49
187
  export const loadCapacitorContacts = () =>
50
- loadCapacitorModule("contacts", () => import("@capacitor-community/contacts"));
188
+ loadCapacitorModule("contacts", () =>
189
+ importNativeModule<CapacitorContactsModule>(capacitorCommunityPackage("contacts")),
190
+ );
51
191
 
52
- export const loadCapacitorDevice = () => loadCapacitorModule("device", () => import("@capacitor/device"));
192
+ export const loadCapacitorCore = () =>
193
+ loadCapacitorModule("core", () => importNativeModule<CapacitorCoreModule>(capacitorPackage("core")));
53
194
 
54
- export const loadCapacitorFcm = () => loadCapacitorModule("fcm", () => import("@capacitor-community/fcm"));
195
+ export const loadCapacitorDevice = () =>
196
+ loadCapacitorModule("device", () => importNativeModule<CapacitorDeviceModule>(capacitorPackage("device")));
197
+
198
+ export const loadCapacitorFcm = () =>
199
+ loadCapacitorModule("fcm", () => importNativeModule<CapacitorFcmModule>(capacitorCommunityPackage("fcm")));
55
200
 
56
201
  export const loadCapacitorGeolocation = () =>
57
- loadCapacitorModule("geolocation", () => import("@capacitor/geolocation"));
202
+ loadCapacitorModule("geolocation", () =>
203
+ importNativeModule<CapacitorGeolocationModule>(capacitorPackage("geolocation")),
204
+ );
205
+
206
+ export const loadCapacitorHaptics = () =>
207
+ loadCapacitorModule("haptics", () => importNativeModule<CapacitorHapticsModule>(capacitorPackage("haptics")));
58
208
 
59
- export const loadCapacitorHaptics = () => loadCapacitorModule("haptics", () => import("@capacitor/haptics"));
209
+ export const loadCapacitorKeyboard = () =>
210
+ loadCapacitorModule("keyboard", () => importNativeModule<CapacitorKeyboardModule>(capacitorPackage("keyboard")));
60
211
 
61
- export const loadCapacitorKeyboard = () => loadCapacitorModule("keyboard", () => import("@capacitor/keyboard"));
212
+ export const loadCapacitorPreferences = () =>
213
+ loadCapacitorModule("preferences", () =>
214
+ importNativeModule<CapacitorPreferencesModule>(capacitorPackage("preferences")),
215
+ );
62
216
 
63
217
  export const loadCapacitorPushNotifications = () =>
64
- loadCapacitorModule("pushNotifications", () => import("@capacitor/push-notifications"));
218
+ loadCapacitorModule("pushNotifications", () =>
219
+ importNativeModule<CapacitorPushNotificationsModule>(capacitorPackage("push-notifications")),
220
+ );
65
221
 
66
- export const loadCapacitorSafeArea = () => loadCapacitorModule("safeArea", () => import("capacitor-plugin-safe-area"));
222
+ export const loadCapacitorSafeArea = () =>
223
+ loadCapacitorModule("safeArea", () => importNativeModule<CapacitorSafeAreaModule>("capacitor-plugin-safe-area"));
67
224
 
68
- export const loadCapacitorUpdater = () => loadCapacitorModule("updater", () => import("@capgo/capacitor-updater"));
225
+ export const loadCapacitorUpdater = () =>
226
+ loadCapacitorModule("updater", () => importNativeModule<CapacitorUpdaterModule>("@capgo/capacitor-updater"));
package/client/cookie.ts CHANGED
@@ -2,6 +2,7 @@ import { getEnv } from "akanjs/base";
2
2
  import { decodeJwtPayload, Logger } from "akanjs/common";
3
3
  import type { Account } from "akanjs/fetch";
4
4
  import { requestStorage } from "akanjs/fetch";
5
+ import { loadCapacitorCore } from "./capacitor";
5
6
  import { storage } from "./storage";
6
7
  import { fetch } from "./useClient";
7
8
 
@@ -42,11 +43,18 @@ export const setCookie = (
42
43
  value: string,
43
44
  options: CookieOptions = { path: "/", sameSite: "none", secure: true },
44
45
  ) => {
45
- if (getEnv().side === "server") return;
46
- else
47
- void import("@capacitor/core").then(({ CapacitorCookies }) =>
48
- CapacitorCookies.setCookie({ key, value, path: options.path }),
49
- );
46
+ const env = getEnv();
47
+ if (env.side === "server") return;
48
+ const encoded = `${key}=${value}`;
49
+ const path = options.path ? `; path=${options.path}` : "";
50
+ const sameSite = options.sameSite ? `; SameSite=${options.sameSite}` : "";
51
+ const secure = options.secure ? "; Secure" : "";
52
+
53
+ document.cookie = `${encoded}${path}${sameSite}${secure}`;
54
+ if (env.renderMode !== "csr") return;
55
+ void loadCapacitorCore()
56
+ .then(({ CapacitorCookies }) => CapacitorCookies.setCookie({ key, value, path: options.path }))
57
+ .catch(() => undefined);
50
58
  };
51
59
 
52
60
  export const getCookie = (key: string): string | undefined => {
@@ -61,6 +69,7 @@ export const getCookie = (key: string): string | undefined => {
61
69
  export const removeCookie = (key: string, options: { path: string } = { path: "/" }) => {
62
70
  if (getEnv().side === "server") return cookies().delete(key);
63
71
  else {
72
+
64
73
  document.cookie = `${key}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`;
65
74
  }
66
75
  };
package/client/device.ts CHANGED
@@ -1,19 +1,87 @@
1
1
  "use client";
2
- import type { DeviceInfo } from "@capacitor/device";
3
- import type { KeyboardInfo } from "@capacitor/keyboard";
4
2
  import type { RefObject } from "react";
5
- import { isMobile } from "react-device-detect";
6
- import { loadCapacitorDevice, loadCapacitorHaptics, loadCapacitorKeyboard, loadCapacitorSafeArea } from "./capacitor";
3
+ import type {
4
+ CapacitorDeviceInfo,
5
+ CapacitorHapticsModule,
6
+ CapacitorKeyboardInfo,
7
+ CapacitorKeyboardModule,
8
+ } from "./capacitor";
9
+
10
+ type DeviceInfo = CapacitorDeviceInfo;
11
+ type Keyboard = CapacitorKeyboardModule["Keyboard"];
12
+ type Haptics = CapacitorHapticsModule["Haptics"];
13
+ type ImpactStyle = CapacitorHapticsModule["ImpactStyle"];
14
+ type ProcessEnvLike = { env?: Record<string, string | undefined> };
15
+
16
+ const globalWithProcess = globalThis as typeof globalThis & { process?: ProcessEnvLike };
7
17
 
8
18
  interface DeviceInitOption {
9
19
  lang: string;
10
20
  info: DeviceInfo;
11
21
  topSafeArea: number;
12
22
  bottomSafeArea: number;
13
- keyboard: Awaited<ReturnType<typeof loadCapacitorKeyboard>>["Keyboard"];
14
- haptics: Awaited<ReturnType<typeof loadCapacitorHaptics>>["Haptics"];
15
- impactStyle: Awaited<ReturnType<typeof loadCapacitorHaptics>>["ImpactStyle"];
23
+ keyboard: Keyboard;
24
+ haptics: Haptics;
25
+ impactStyle: ImpactStyle;
16
26
  }
27
+
28
+ const noopKeyboard: Keyboard = {
29
+ show: async () => undefined,
30
+ hide: async () => undefined,
31
+ addListener: async () => undefined,
32
+ removeAllListeners: async () => undefined,
33
+ };
34
+
35
+ const noopHaptics: Haptics = {
36
+ vibrate: async () => undefined,
37
+ impact: async () => undefined,
38
+ selectionStart: async () => undefined,
39
+ selectionChanged: async () => undefined,
40
+ selectionEnd: async () => undefined,
41
+ };
42
+
43
+ const noopImpactStyle: ImpactStyle = {
44
+ Light: "light",
45
+ Medium: "medium",
46
+ Heavy: "heavy",
47
+ };
48
+
49
+ const getRenderMode = () => globalWithProcess.process?.env?.AKAN_PUBLIC_RENDER_ENV ?? "csr";
50
+
51
+ const getBrowserLanguage = () => globalThis.navigator?.language?.split("-")[0] ?? "en";
52
+
53
+ const isNativeTarget = () => {
54
+ if (typeof window === "undefined") return false;
55
+ return Boolean((window as typeof window & { __AKAN_MOBILE_TARGET__?: unknown }).__AKAN_MOBILE_TARGET__);
56
+ };
57
+
58
+ export const isMobileDevice = () => {
59
+ if (typeof navigator === "undefined") return false;
60
+ if (typeof window !== "undefined" && window.matchMedia?.("(hover: none) and (pointer: coarse)")?.matches) return true;
61
+ return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
62
+ };
63
+
64
+ const createWebDevice = ({
65
+ lang,
66
+ supportLanguages,
67
+ }: {
68
+ lang?: string;
69
+ supportLanguages: string[] | readonly string[];
70
+ }) => {
71
+ const pathname = typeof window === "undefined" ? "" : window.location.pathname;
72
+ const predefinedLangPath = pathname.split("/")[1]?.split("?")[0];
73
+ const predefinedLang = supportLanguages.find((language) => language === predefinedLangPath);
74
+ return new Device({
75
+ lang: lang ?? predefinedLang ?? getBrowserLanguage(),
76
+ info: { platform: "web", isVirtual: false, osVersion: "" },
77
+ topSafeArea: 0,
78
+ bottomSafeArea: 0,
79
+ keyboard: noopKeyboard,
80
+ haptics: noopHaptics,
81
+ impactStyle: noopImpactStyle,
82
+ });
83
+ };
84
+
17
85
  /** Capacitor-aware device helper for platform info, safe areas, keyboard, haptics, and scroll state. */
18
86
  export class Device {
19
87
  static instance: Device | null = null;
@@ -25,6 +93,14 @@ export class Device {
25
93
  supportLanguages?: string[] | readonly string[];
26
94
  }) {
27
95
  if (Device.instance) return Device.instance;
96
+ if (getRenderMode() !== "csr" || !isNativeTarget()) {
97
+ const device = createWebDevice({ lang, supportLanguages });
98
+ Device.instance = device;
99
+ return device;
100
+ }
101
+ const { loadCapacitorDevice, loadCapacitorHaptics, loadCapacitorKeyboard, loadCapacitorSafeArea } = await import(
102
+ "./capacitor"
103
+ );
28
104
  const [{ Device: CapacitorDevice }, { Keyboard }, { Haptics, ImpactStyle }, { SafeArea }] = await Promise.all([
29
105
  loadCapacitorDevice(),
30
106
  loadCapacitorKeyboard(),
@@ -61,7 +137,7 @@ export class Device {
61
137
  lang: string;
62
138
  topSafeArea: number;
63
139
  bottomSafeArea: number;
64
- isMobile = isMobile;
140
+ isMobile = isMobileDevice();
65
141
  #keyboard: DeviceInitOption["keyboard"];
66
142
  #haptics: DeviceInitOption["haptics"];
67
143
  #impactStyle: DeviceInitOption["impactStyle"];
@@ -89,10 +165,10 @@ export class Device {
89
165
  }
90
166
  listenKeyboardChanged(onKeyboardChanged: (height: number) => void) {
91
167
  if (this.info.platform === "web") return;
92
- void this.#keyboard.addListener("keyboardWillShow", (keyboard: KeyboardInfo) => {
168
+ void this.#keyboard.addListener("keyboardWillShow", (keyboard: CapacitorKeyboardInfo) => {
93
169
  onKeyboardChanged(keyboard.keyboardHeight);
94
170
  });
95
- void this.#keyboard.addListener("keyboardDidShow", (keyboard: KeyboardInfo) => {
171
+ void this.#keyboard.addListener("keyboardDidShow", (keyboard: CapacitorKeyboardInfo) => {
96
172
  onKeyboardChanged(keyboard.keyboardHeight);
97
173
  });
98
174
  void this.#keyboard.addListener("keyboardWillHide", () => {
package/client/index.ts CHANGED
@@ -1,4 +1,3 @@
1
- export * from "./capacitor";
2
1
  export * from "./clientRuntime";
3
2
  export * from "./cookie";
4
3
  export * from "./createFont";
package/client/storage.ts CHANGED
@@ -1,33 +1,57 @@
1
1
  import { getEnv } from "akanjs/base";
2
+ import { loadCapacitorPreferences } from "./capacitor";
3
+
4
+ const getLocalStorageItem = (key: string) => localStorage.getItem(key);
5
+
6
+ const setLocalStorageItem = (key: string, value: string) => {
7
+ localStorage.setItem(key, value);
8
+ };
9
+
10
+ const removeLocalStorageItem = (key: string) => {
11
+ localStorage.removeItem(key);
12
+ };
2
13
 
3
14
  export const storage = {
4
15
  getItem: async (key: string) => {
5
- if (getEnv().side === "server") return;
6
- if (getEnv().renderMode === "ssr") return localStorage.getItem(key);
7
- else {
8
- const { Preferences } = await import("@capacitor/preferences");
16
+ const env = getEnv();
17
+ if (env.side === "server") return;
18
+ if (env.renderMode === "ssr") return getLocalStorageItem(key);
19
+ try {
20
+ const { Preferences } = await loadCapacitorPreferences();
9
21
  return (await Preferences.get({ key })).value;
22
+ } catch {
23
+ return getLocalStorageItem(key);
10
24
  }
11
25
  },
12
26
  setItem: async (key: string, value: string) => {
13
- if (getEnv().side === "server") return;
14
- if (getEnv().renderMode === "ssr") {
15
- localStorage.setItem(key, value);
27
+ const env = getEnv();
28
+ if (env.side === "server") return;
29
+ if (env.renderMode === "ssr") {
30
+ setLocalStorageItem(key, value);
16
31
  return;
17
- } else {
18
- const { Preferences } = await import("@capacitor/preferences");
32
+ }
33
+ try {
34
+ const { Preferences } = await loadCapacitorPreferences();
19
35
  await Preferences.set({ key, value });
20
36
  return;
37
+ } catch {
38
+ setLocalStorageItem(key, value);
39
+ return;
21
40
  }
22
41
  },
23
42
  removeItem: async (key: string) => {
24
- if (getEnv().side === "server") return;
25
- if (getEnv().renderMode === "ssr") {
26
- localStorage.removeItem(key);
43
+ const env = getEnv();
44
+ if (env.side === "server") return;
45
+ if (env.renderMode === "ssr") {
46
+ removeLocalStorageItem(key);
27
47
  return;
28
- } else {
29
- const { Preferences } = await import("@capacitor/preferences");
48
+ }
49
+ try {
50
+ const { Preferences } = await loadCapacitorPreferences();
30
51
  return Preferences.remove({ key });
52
+ } catch {
53
+ removeLocalStorageItem(key);
54
+ return;
31
55
  }
32
56
  },
33
57
  };
@@ -93,6 +93,11 @@ export const getFilterSortByKey = (modelRef: FilterCls, key: string) => {
93
93
  return filterMeta.sort[key];
94
94
  };
95
95
 
96
+ export const fillMissingFilterArgs = (filterInfo: FilterInfo, args: unknown[]) => {
97
+ if (args.length >= filterInfo.args.length) return args;
98
+ return [...args, ...Array(filterInfo.args.length - args.length).fill(undefined)];
99
+ };
100
+
96
101
  export type BaseFilterSortKey = "latest" | "oldest";
97
102
  export type BaseFilterQueryKey = "any";
98
103
  export type BaseFilterKey = BaseFilterSortKey | BaseFilterQueryKey;
@@ -19,6 +19,16 @@ import { WsClient } from "./wsClient";
19
19
  type FetchHandler = (...args: unknown[]) => PromiseOrObject<unknown>;
20
20
  type UnknownRecord = Record<string, unknown>;
21
21
 
22
+ const isNullableArg = (arg: SerializedArg) => arg.nullable ?? arg.type === "search";
23
+
24
+ const normalizeQueryArgs = (queryArgs: unknown[], args: SerializedArg[]) => {
25
+ let length = Math.min(queryArgs.length, args.length);
26
+ while (length > 0 && isNullableArg(args[length - 1]) && queryArgs[length - 1] == null) length--;
27
+ return queryArgs.slice(0, length);
28
+ };
29
+
30
+ const expandQueryArgs = (queryArgs: unknown[], args: SerializedArg[]) => args.map((_, idx) => queryArgs[idx]);
31
+
22
32
  export type FetchProxy<
23
33
  FetchType = unknown,
24
34
  SliceMetaObj extends Record<string, SliceMeta> = Record<never, never>,
@@ -411,14 +421,18 @@ export class FetchClient {
411
421
  const listFn = this.handler[names.list] as (...args: unknown[]) => Promise<unknown[]>;
412
422
  const insightFn = this.handler[names.insight] as (...args: unknown[]) => Promise<unknown>;
413
423
  const initFn = async (...argData: unknown[]) => {
414
- const queryArgs = Array.from({ length: argLength }, (_, idx) => argData[idx]);
424
+ const queryArgs = normalizeQueryArgs(
425
+ Array.from({ length: Math.min(argData.length, argLength) }, (_, idx) => argData[idx]),
426
+ slice.args,
427
+ );
428
+ const fetchQueryArgs = expandQueryArgs(queryArgs, slice.args);
415
429
  const option = (argData[argLength] ?? {}) as { page?: number; limit?: number; sort?: string; insight?: boolean };
416
430
  const { page = 1, limit = 20, sort = "latest", insight: fetchInsight = true } = option;
417
431
  const skip = (page - 1) * limit;
418
432
 
419
433
  const [modelObjList, modelObjInsight] = (await Promise.all([
420
- listFn(...queryArgs, skip, limit, sort, { ...option, crystalize: false }),
421
- fetchInsight ? insightFn(...queryArgs, { ...option, crystalize: false }) : null,
434
+ listFn(...fetchQueryArgs, skip, limit, sort, { ...option, crystalize: false }),
435
+ fetchInsight ? insightFn(...fetchQueryArgs, { ...option, crystalize: false }) : null,
422
436
  ])) as unknown as [BaseObject[], BaseInsight];
423
437
  const modelList = new DataList(modelObjList.map((modelObj) => new cnst.light(modelObj)));
424
438
  const modelInsight = new cnst.insight(modelObjInsight);