akanjs 2.3.13 → 2.4.0-rc.1

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 (49) hide show
  1. package/client/csrTypes.ts +21 -0
  2. package/client/frameConfig.ts +7 -1
  3. package/package.json +5 -1
  4. package/runtime/logs/serverGet-local-local-2026-07-21-gateway-0001.log +4 -0
  5. package/runtime/logs/serverGet-local-local-2026-07-21-gateway-0002.log +5 -0
  6. package/runtime/logs/serverGet-local-local-2026-07-21-gateway-0003.log +7 -0
  7. package/runtime/logs/serverGet-local-local-2026-07-21-gateway-0004.log +2 -0
  8. package/server/akanApp.ts +128 -26
  9. package/server/akanServer.ts +46 -8
  10. package/server/lifecycle/portInUse.ts +11 -0
  11. package/server/routeElementComposer.tsx +22 -3
  12. package/server/routeTreeBuilder.ts +5 -0
  13. package/server/rscWorker.tsx +16 -0
  14. package/server/rscWorkerCache.ts +2 -0
  15. package/server/ssrFromRscRenderer.tsx +2 -1
  16. package/server/ssrTypes.ts +14 -0
  17. package/server/webRouter.ts +1 -0
  18. package/service/ipcTypes.ts +3 -1
  19. package/signal/guard.ts +3 -3
  20. package/signal/signalContext.ts +5 -0
  21. package/store/baseSt.ts +1 -0
  22. package/types/client/csrTypes.d.ts +19 -0
  23. package/types/server/lifecycle/portInUse.d.ts +5 -0
  24. package/types/server/routeElementComposer.d.ts +7 -3
  25. package/types/server/rscWorkerCache.d.ts +2 -0
  26. package/types/server/ssrTypes.d.ts +14 -0
  27. package/types/service/ipcTypes.d.ts +5 -1
  28. package/types/signal/guard.d.ts +2 -2
  29. package/types/signal/signalContext.d.ts +1 -0
  30. package/types/ui/Load/Units.d.ts +1 -1
  31. package/types/ui/Model/LoadView.d.ts +10 -0
  32. package/types/ui/Model/index.d.ts +1 -0
  33. package/types/ui/Model/index_.d.ts +1 -0
  34. package/types/webkit/index.d.ts +4 -0
  35. package/types/webkit/useCamera.d.ts +19 -0
  36. package/types/webkit/useCodepush.d.ts +16 -0
  37. package/types/webkit/useContact.d.ts +11 -0
  38. package/types/webkit/useGeoLocation.d.ts +8 -0
  39. package/types/webkit/usePurchase.d.ts +19 -0
  40. package/ui/Load/Units.tsx +2 -2
  41. package/ui/Model/LoadView.tsx +11 -0
  42. package/ui/Model/index.ts +2 -0
  43. package/ui/Model/index_.tsx +1 -0
  44. package/webkit/index.ts +4 -0
  45. package/webkit/useCamera.tsx +103 -0
  46. package/webkit/useCodepush.tsx +99 -0
  47. package/webkit/useContact.tsx +52 -0
  48. package/webkit/useGeoLocation.tsx +24 -0
  49. package/webkit/usePurchase.tsx +156 -0
package/ui/Model/index.ts CHANGED
@@ -4,6 +4,7 @@ import {
4
4
  EditModal,
5
5
  EditWrapper,
6
6
  LoadInit,
7
+ LoadView,
7
8
  New,
8
9
  NewWrapper,
9
10
  Remove,
@@ -25,6 +26,7 @@ export const Model = {
25
26
  Remove,
26
27
  RemoveWrapper,
27
28
  LoadInit,
29
+ LoadView,
28
30
  ViewWrapper,
29
31
  ViewEditModal,
30
32
  Edit,
@@ -9,6 +9,7 @@ export const NewWrapper = lazy(() => import("./NewWrapper"));
9
9
  export const EditWrapper = lazy(() => import("./EditWrapper"));
10
10
  export const RemoveWrapper = lazy(() => import("./RemoveWrapper"));
11
11
  export const LoadInit = lazy(() => import("./LoadInit"));
12
+ export const LoadView = lazy(() => import("./LoadView"));
12
13
  export const ViewWrapper = lazy(() => import("./ViewWrapper"));
13
14
  export const ViewEditModal = lazy(() => import("./ViewEditModal"));
14
15
  export const Edit = lazy(() => import("./Edit"));
package/webkit/index.ts CHANGED
@@ -3,9 +3,13 @@ export { createRobotPage } from "./createRobotPage";
3
3
  export { createSitemapPage } from "./createSitemapPage";
4
4
  export { lazy } from "./lazy";
5
5
  export type * from "./types";
6
+ export { useCamera } from "./useCamera";
7
+ export { useCodepush } from "./useCodepush";
8
+ export { useContact } from "./useContact";
6
9
  export { useCsrValues } from "./useCsrValues";
7
10
  export { useDebounce } from "./useDebounce";
8
11
  export { useFetch, useFetchFn } from "./useFetch";
12
+ export { useGeoLocation } from "./useGeoLocation";
9
13
  export { useHistory } from "./useHistory";
10
14
  export { useInterval } from "./useInterval";
11
15
  export { useLocation } from "./useLocation";
@@ -0,0 +1,103 @@
1
+ "use client";
2
+ import { Device, isMobileDevice } from "akanjs/client";
3
+ import { type CapacitorPermissionState, loadCapacitorCamera } from "akanjs/client/capacitor";
4
+ import { useEffect, useState } from "react";
5
+
6
+ type PermissionStatus = {
7
+ camera: CapacitorPermissionState;
8
+ photos: CapacitorPermissionState;
9
+ };
10
+
11
+ /** Capacitor camera/photos hook with permission checks and app-settings fallback. */
12
+ export const useCamera = () => {
13
+ const [permissions, setPermissions] = useState<PermissionStatus>({ camera: "prompt", photos: "prompt" });
14
+
15
+ /**
16
+ * 최초로 킬 경우 권한은 prompt 상태이다.
17
+ * prompt 상태일 경우 권한을 요청한다.
18
+ * 권한이 denied 상태일 경우 설정으로 이동한다.
19
+ * 이후 state의 permission을 업데이트해야한다.
20
+ *
21
+ */
22
+ const checkPermission = async (type: "photos" | "camera" | "all") => {
23
+ try {
24
+ const { Camera } = await loadCapacitorCamera();
25
+ if (type === "photos") {
26
+ if (permissions.photos === "prompt") {
27
+ const { photos } = await Camera.requestPermissions();
28
+ setPermissions((prev) => ({ ...prev, photos }));
29
+ } else if (permissions.photos === "denied") {
30
+ location.assign("app-settings:");
31
+ return;
32
+ }
33
+ } else if (type === "camera") {
34
+ if (permissions.camera === "prompt") {
35
+ const { camera } = await Camera.requestPermissions();
36
+ setPermissions((prev) => ({ ...prev, camera }));
37
+ } else if (permissions.camera === "denied") {
38
+ location.assign("app-settings:");
39
+ return;
40
+ }
41
+ } else {
42
+ if (permissions.camera === "prompt" || permissions.photos === "prompt") {
43
+ const permissions = await Camera.requestPermissions();
44
+ setPermissions(permissions);
45
+ } else if (permissions.camera === "denied" || permissions.photos === "denied") {
46
+ location.assign("app-settings:");
47
+ return;
48
+ }
49
+ }
50
+ } catch {
51
+ }
52
+ };
53
+
54
+ const getPhoto = async (src: "prompt" | "camera" | "photos" = "prompt") => {
55
+ const { Camera, CameraResultType, CameraSource } = await loadCapacitorCamera();
56
+ const source =
57
+ Device.getDevice().info.platform !== "web"
58
+ ? src === "prompt"
59
+ ? CameraSource.Prompt
60
+ : src === "camera"
61
+ ? CameraSource.Camera
62
+ : CameraSource.Photos
63
+ : CameraSource.Photos;
64
+ const permission = src === "prompt" ? "all" : src === "camera" ? "camera" : "photos";
65
+ await checkPermission(permission);
66
+ try {
67
+ const photo = await Camera.getPhoto({
68
+ quality: 100,
69
+ source,
70
+ allowEditing: false,
71
+ resultType: CameraResultType.DataUrl,
72
+ promptLabelHeader: "프로필 사진을 올려주세요",
73
+ promptLabelPhoto: "앨범에서 선택하기",
74
+ promptLabelPicture: "사진 찍기",
75
+ promptLabelCancel: "취소",
76
+ });
77
+ return photo;
78
+ } catch (e) {
79
+ if (e === "User cancelled photos app") return;
80
+ }
81
+ };
82
+
83
+ const pickImage = async () => {
84
+ await checkPermission("photos");
85
+ const { Camera } = await loadCapacitorCamera();
86
+ const photo = await Camera.pickImages({
87
+ quality: 90,
88
+ });
89
+
90
+ return photo;
91
+ };
92
+
93
+ useEffect(() => {
94
+ void (async () => {
95
+ if (isMobileDevice()) {
96
+ const { Camera } = await loadCapacitorCamera();
97
+ const permissions = await Camera.checkPermissions();
98
+ setPermissions(permissions);
99
+ }
100
+ })();
101
+ }, []);
102
+ return { permissions, getPhoto, pickImage, checkPermission };
103
+ };
@@ -0,0 +1,99 @@
1
+ "use client";
2
+ import { loadCapacitorApp, loadCapacitorDevice, loadCapacitorUpdater } from "akanjs/client/capacitor";
3
+ import { HttpClient, mergeVersion, splitVersion } from "akanjs/common";
4
+ import type { ProtoAppInfo, ProtoFile } from "akanjs/constant";
5
+ import { useState } from "react";
6
+
7
+ export const useCodepush = ({ serverUrl }: { serverUrl: string }) => {
8
+ const [update, setUpdate] = useState(false);
9
+ const [version, setVersion] = useState("");
10
+
11
+ const initialize = async () => {
12
+ const { CapacitorUpdater } = await loadCapacitorUpdater();
13
+ await CapacitorUpdater.notifyAppReady();
14
+ };
15
+ const checkNewRelease = async () => {
16
+ const [{ App }, { Device }, { CapacitorUpdater }] = await Promise.all([
17
+ loadCapacitorApp(),
18
+ loadCapacitorDevice(),
19
+ loadCapacitorUpdater(),
20
+ ]);
21
+
22
+ const info = await Device.getInfo();
23
+ const app = await App.getInfo();
24
+ const pluginVersion = await CapacitorUpdater.getPluginVersion();
25
+ const { deviceId } = await CapacitorUpdater.getDeviceId();
26
+ const { bundle: version, native } = await CapacitorUpdater.current();
27
+ const builtInversion = await CapacitorUpdater.getBuiltinVersion();
28
+ const appId = app.id;
29
+ const platform = info.platform;
30
+
31
+ window.alert(
32
+ `getBuildinVersion:${builtInversion.version}\ncurrent.bundle:${version.version}\ncurrennt.native:${native}`,
33
+ );
34
+ /**
35
+ * "version_name": "builtin",
36
+ * "version_code": "1",
37
+ * "app_id": "com.lu.app",
38
+ * "plugin_version": "5.6.9",
39
+ * "version_build": "1.0",
40
+ * "is_prod": true,
41
+ * "version_os": "17.0.1",
42
+ * "is_emulator": true,
43
+ * "custom_id": "",
44
+ * "device_id": "C77000B1-7D28-4697-ADE0-74452F47C350",
45
+ * "platform": "ios",
46
+ * "defaultChannel": ""
47
+ */
48
+ const { major, minor, patch } = splitVersion(version.version === "builtin" ? app.version : version.version);
49
+ const appName = process.env.AKAN_PUBLIC_APP_NAME ?? "";
50
+
51
+ const appInfo: ProtoAppInfo = {
52
+ appId,
53
+ appName,
54
+ deviceId: deviceId,
55
+ platform: platform as "ios" | "android",
56
+ branch: process.env.AKAN_PUBLIC_ENV ?? "debug",
57
+ isEmulator: info.isVirtual,
58
+ major: parseInt(major),
59
+ minor: parseInt(minor),
60
+ patch: parseInt(patch),
61
+ buildNum: app.build, //앱내 빌드시 버전 횟수 모르면 고한테 물어보기
62
+ versionOs: info.osVersion,
63
+ };
64
+
65
+ const url = serverUrl.replace("lu", "akasys");
66
+ const httpClient = new HttpClient(url);
67
+ const release = await httpClient.post<(ProtoAppInfo & { appBuild: string }) | null>("/release/codepush", {
68
+ data: { ...appInfo },
69
+ });
70
+ if (!release) return;
71
+ const file = await httpClient.get<ProtoFile>(`/file/file/${release.appBuild}`);
72
+
73
+ return { release: release, bundleFile: file };
74
+ };
75
+
76
+ const codepush = async () => {
77
+
78
+ const newRelease = await checkNewRelease();
79
+ if (!newRelease) return;
80
+ const { release, bundleFile } = newRelease;
81
+ const { CapacitorUpdater } = await loadCapacitorUpdater();
82
+ setUpdate(true);
83
+ const bundle = await CapacitorUpdater.download({
84
+ url: bundleFile.url,
85
+ version: mergeVersion(release.major, release.minor, release.patch),
86
+ });
87
+
88
+ await CapacitorUpdater.set(bundle);
89
+ };
90
+ const getVersion = async () => {
91
+ const { CapacitorUpdater } = await loadCapacitorUpdater();
92
+ return await CapacitorUpdater.getBuiltinVersion();
93
+ };
94
+
95
+ const statManager = async () => {
96
+ };
97
+
98
+ return { update, version, initialize, checkNewRelease, codepush, statManager };
99
+ };
@@ -0,0 +1,52 @@
1
+ "use client";
2
+ import { Device } from "akanjs/client";
3
+ import { type CapacitorPermissionState, loadCapacitorContacts } from "akanjs/client/capacitor";
4
+ import { useEffect, useState } from "react";
5
+
6
+ type PermissionStatus = {
7
+ contacts: CapacitorPermissionState;
8
+ };
9
+
10
+ /** Capacitor contacts hook with permission checks and contact loading helpers. */
11
+ export const useContact = () => {
12
+ const [permissions, setPermissions] = useState<PermissionStatus>({ contacts: "prompt" });
13
+
14
+ /**
15
+ * 최초로 킬 경우 권한은 prompt 상태이다.
16
+ * prompt 상태일 경우 권한을 요청한다.
17
+ * 권한이 denied 상태일 경우 설정으로 이동한다.
18
+ * 이후 state의 permission을 업데이트해야한다.
19
+ *
20
+ */
21
+ const checkPermission = async () => {
22
+ try {
23
+ const { Contacts } = await loadCapacitorContacts();
24
+ if (permissions.contacts === "prompt") {
25
+ const { contacts } = await Contacts.requestPermissions();
26
+ setPermissions((prev) => ({ ...prev, contacts }));
27
+ } else if (permissions.contacts === "denied") {
28
+ location.assign("app-settings:");
29
+ return;
30
+ }
31
+ } catch {
32
+ }
33
+ };
34
+
35
+ const getContacts = async () => {
36
+ await checkPermission();
37
+ const { Contacts } = await loadCapacitorContacts();
38
+ const { contacts } = await Contacts.getContacts({ projection: { name: true, phones: true } });
39
+ return contacts;
40
+ };
41
+
42
+ useEffect(() => {
43
+ void (async () => {
44
+ if (Device.getDevice().info.platform === "web") return;
45
+ const { Contacts } = await loadCapacitorContacts();
46
+ const permissions = await Contacts.checkPermissions();
47
+ setPermissions(permissions);
48
+ })();
49
+ }, []);
50
+
51
+ return { permissions, getContacts, checkPermission };
52
+ };
@@ -0,0 +1,24 @@
1
+ "use client";
2
+ import { loadCapacitorGeolocation } from "akanjs/client/capacitor";
3
+
4
+ /** Capacitor geolocation hook with permission checks and current position lookup. */
5
+ export const useGeoLocation = () => {
6
+ const checkPermission = async (): Promise<{ geolocation: string; coarseLocation: string }> => {
7
+ const { Geolocation } = await loadCapacitorGeolocation();
8
+ const { location: geolocation, coarseLocation } = await Geolocation.requestPermissions();
9
+ return { geolocation, coarseLocation };
10
+ };
11
+
12
+ const getPosition = async () => {
13
+ const { geolocation, coarseLocation } = await checkPermission();
14
+ if (geolocation === "denied" || coarseLocation === "denied") {
15
+ location.assign("app-settings:");
16
+ return;
17
+ }
18
+ const { Geolocation } = await loadCapacitorGeolocation();
19
+ const coordinates = await Geolocation.getCurrentPosition();
20
+ return coordinates;
21
+ };
22
+
23
+ return { checkPermission, getPosition };
24
+ };
@@ -0,0 +1,156 @@
1
+ "use client";
2
+
3
+ import "cordova-plugin-purchase/www/store";
4
+
5
+ import { loadCapacitorApp } from "akanjs/client/capacitor";
6
+ import { useEffect, useRef, useState } from "react";
7
+
8
+ export type PlatformType = "android" | "ios" | "all";
9
+ export interface ProductType {
10
+ id: string;
11
+ type: keyof typeof CdvPurchase.ProductType;
12
+ }
13
+
14
+ export type CdvProductType = CdvPurchase.ProductType;
15
+
16
+ export const usePurchase = ({
17
+ platform,
18
+ productInfo,
19
+ url,
20
+ onPay,
21
+ onSubscribe,
22
+ }: {
23
+ platform: PlatformType;
24
+ productInfo: ProductType[];
25
+ url: string;
26
+ onPay?: (transaction: CdvPurchase.Transaction) => void | Promise<void>;
27
+ onSubscribe?: (transaction: CdvPurchase.Transaction) => void | Promise<void>;
28
+ }) => {
29
+ const [isLoading, setIsLoading] = useState(true);
30
+ const billingRef = useRef<any>(null);
31
+
32
+ useEffect(() => {
33
+ const init = async () => {
34
+ if (CdvPurchase.store.isReady) {
35
+ setIsLoading(false);
36
+
37
+ return;
38
+ }
39
+ const { App } = await loadCapacitorApp();
40
+ const app = await App.getInfo();
41
+ if (platform === "all")
42
+ CdvPurchase.store.register([
43
+ ...productInfo.map((prouct) => ({
44
+ id: prouct.id,
45
+ platform: CdvPurchase.Platform.GOOGLE_PLAY,
46
+ type: CdvPurchase.ProductType[prouct.type],
47
+ })),
48
+ ...productInfo.map((prouct) => ({
49
+ id: prouct.id,
50
+ platform: CdvPurchase.Platform.APPLE_APPSTORE,
51
+ type: CdvPurchase.ProductType[prouct.type],
52
+ })),
53
+ ]);
54
+ else
55
+ CdvPurchase.store.register(
56
+ productInfo.map((product) => ({
57
+ id: product.id,
58
+ platform: platform === "android" ? CdvPurchase.Platform.GOOGLE_PLAY : CdvPurchase.Platform.APPLE_APPSTORE,
59
+ type: CdvPurchase.ProductType[product.type],
60
+ })),
61
+ );
62
+
63
+ await CdvPurchase.store.initialize([
64
+ { platform: CdvPurchase.Platform.APPLE_APPSTORE, options: { needAppReceipt: false } },
65
+ { platform: CdvPurchase.Platform.GOOGLE_PLAY },
66
+ ]);
67
+ await CdvPurchase.store.update();
68
+ await CdvPurchase.store.restorePurchases();
69
+
70
+ CdvPurchase.store.validator = (async (
71
+ request: { id: string; transaction: { id: string; purchaseToken: string; appStoreReceipt: string } },
72
+ callback: (result: {
73
+ ok: boolean;
74
+ data: { id: string; latest_receipt: boolean; transaction: CdvPurchase.Transaction };
75
+ }) => void,
76
+ ) => {
77
+ const transactionId = request.transaction.id;
78
+ const transactions = CdvPurchase.store.localTransactions;
79
+ const verifingTransaction = transactions.find((transaction) => transaction.transactionId === transactionId);
80
+
81
+ if (verifingTransaction?.state !== "approved") return;
82
+
83
+ const billing = await fetch(`${url}/billing/verifyBilling`, {
84
+ method: "POST",
85
+ headers: {
86
+ "Content-Type": "application/json",
87
+ },
88
+ body: JSON.stringify({
89
+ data: {
90
+ platform: verifingTransaction.platform === CdvPurchase.Platform.GOOGLE_PLAY ? "google" : "apple",
91
+ packageName: app.id,
92
+ productId: verifingTransaction.products[0].id,
93
+ receipt:
94
+ verifingTransaction.platform === CdvPurchase.Platform.GOOGLE_PLAY
95
+ ? request.transaction.purchaseToken
96
+ : request.transaction.appStoreReceipt,
97
+ transactionId: verifingTransaction.transactionId,
98
+ },
99
+ }),
100
+ });
101
+
102
+ billingRef.current = billing.json();
103
+
104
+ callback({
105
+
106
+ ok: !!billing,
107
+
108
+ data: { id: request.id, latest_receipt: true, transaction: request.transaction } as any,
109
+ });
110
+ }) as any;
111
+ if (CdvPurchase.store.localReceipts.length > 0) {
112
+ CdvPurchase.store.localReceipts.forEach((receipt) => {
113
+ if (receipt.platform === CdvPurchase.Platform.GOOGLE_PLAY)
114
+ if (receipt.transactions[0].state === CdvPurchase.TransactionState.APPROVED)
115
+ void receipt.transactions[0].verify();
116
+ else void receipt.transactions[0].finish();
117
+ });
118
+ }
119
+
120
+ CdvPurchase.store
121
+ .when()
122
+ .approved((transaction) => {
123
+ void transaction.verify();
124
+ })
125
+ .verified((receipt) => {
126
+ void receipt.finish();
127
+ })
128
+ .finished((transaction) => {
129
+ void inAppPurchase(transaction);
130
+ });
131
+ setIsLoading(false);
132
+ };
133
+ void init();
134
+ }, []);
135
+ const purchaseProduct = async (product: CdvPurchase.Product) => {
136
+ await product.getOffer()?.order();
137
+ };
138
+
139
+ const restorePurchases = async () => {
140
+ await CdvPurchase.store.restorePurchases();
141
+ };
142
+
143
+ const inAppPurchase = async (transaction: CdvPurchase.Transaction) => {
144
+ const product = CdvPurchase.store.get(transaction.products[0].id);
145
+ if (product?.type === "consumable") await onPay?.(transaction);
146
+ else await onSubscribe?.(transaction);
147
+ await transaction.finish();
148
+ };
149
+
150
+ return {
151
+ isLoading,
152
+ products: CdvPurchase.store.products,
153
+ purchaseProduct,
154
+ restorePurchases,
155
+ };
156
+ };