@jskit-ai/mobile-capacitor 0.1.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.
@@ -0,0 +1,136 @@
1
+ const API_ROUTE_PREFIXES = Object.freeze([
2
+ "/api",
3
+ "/socket.io"
4
+ ]);
5
+
6
+ function parseAbsoluteHttpUrl(url = "", {
7
+ apiBaseUrl = "",
8
+ emptyUrlMessage = "URL is required.",
9
+ missingApiBaseUrlMessage = "config.mobile.apiBaseUrl is required.",
10
+ invalidApiBaseUrlMessage = "config.mobile.apiBaseUrl must be a valid absolute URL.",
11
+ invalidApiBaseUrlProtocolMessage = "config.mobile.apiBaseUrl must use http or https.",
12
+ invalidUrlProtocolMessage = "URL must use http or https."
13
+ } = {}) {
14
+ const normalizedUrl = String(url || "").trim();
15
+ if (!normalizedUrl) {
16
+ throw new Error(emptyUrlMessage);
17
+ }
18
+
19
+ let absoluteUrl = null;
20
+ try {
21
+ absoluteUrl = new URL(normalizedUrl);
22
+ } catch {}
23
+
24
+ if (absoluteUrl) {
25
+ const protocol = String(absoluteUrl.protocol || "").toLowerCase();
26
+ if (protocol !== "http:" && protocol !== "https:") {
27
+ throw new Error(invalidUrlProtocolMessage);
28
+ }
29
+ return absoluteUrl.toString();
30
+ }
31
+
32
+ const normalizedApiBaseUrl = String(apiBaseUrl || "").trim();
33
+ if (!normalizedApiBaseUrl) {
34
+ throw new Error(missingApiBaseUrlMessage);
35
+ }
36
+
37
+ let baseUrl;
38
+ try {
39
+ baseUrl = new URL(normalizedApiBaseUrl);
40
+ } catch {
41
+ throw new Error(invalidApiBaseUrlMessage);
42
+ }
43
+
44
+ const baseProtocol = String(baseUrl.protocol || "").toLowerCase();
45
+ if (baseProtocol !== "http:" && baseProtocol !== "https:") {
46
+ throw new Error(invalidApiBaseUrlProtocolMessage);
47
+ }
48
+
49
+ return new URL(normalizedUrl, baseUrl).toString();
50
+ }
51
+
52
+ function resolveCapacitorAbsoluteHttpUrl(url = "", apiBaseUrl = "", messages = {}) {
53
+ return parseAbsoluteHttpUrl(url, {
54
+ ...messages,
55
+ apiBaseUrl
56
+ });
57
+ }
58
+
59
+ function isCapacitorApiRequestTarget(url = "") {
60
+ const normalizedUrl = String(url || "").trim();
61
+ if (!normalizedUrl.startsWith("/")) {
62
+ return false;
63
+ }
64
+ return API_ROUTE_PREFIXES.some((prefix) => normalizedUrl === prefix || normalizedUrl.startsWith(`${prefix}/`));
65
+ }
66
+
67
+ function normalizeFetchInput(input = "") {
68
+ if (typeof input === "string") {
69
+ return Object.freeze({
70
+ rawUrl: input,
71
+ rebuild(nextUrl) {
72
+ return nextUrl;
73
+ }
74
+ });
75
+ }
76
+
77
+ if (typeof URL === "function" && input instanceof URL) {
78
+ return Object.freeze({
79
+ rawUrl: input.toString(),
80
+ rebuild(nextUrl) {
81
+ return nextUrl;
82
+ }
83
+ });
84
+ }
85
+
86
+ if (typeof Request === "function" && input instanceof Request) {
87
+ return Object.freeze({
88
+ rawUrl: String(input.url || ""),
89
+ rebuild(nextUrl) {
90
+ return new Request(nextUrl, input);
91
+ }
92
+ });
93
+ }
94
+
95
+ return Object.freeze({
96
+ rawUrl: "",
97
+ rebuild() {
98
+ return input;
99
+ }
100
+ });
101
+ }
102
+
103
+ function createCapacitorAwareFetch({
104
+ fetchImpl = globalThis.fetch,
105
+ adapter = null,
106
+ apiBaseUrl = ""
107
+ } = {}) {
108
+ if (typeof fetchImpl !== "function") {
109
+ throw new Error("createCapacitorAwareFetch requires fetchImpl.");
110
+ }
111
+
112
+ return async function capacitorAwareFetch(input, init) {
113
+ if (adapter?.available !== true) {
114
+ return fetchImpl(input, init);
115
+ }
116
+
117
+ const normalizedInput = normalizeFetchInput(input);
118
+ if (!isCapacitorApiRequestTarget(normalizedInput.rawUrl)) {
119
+ return fetchImpl(input, init);
120
+ }
121
+
122
+ const resolvedUrl = resolveCapacitorAbsoluteHttpUrl(normalizedInput.rawUrl, apiBaseUrl, {
123
+ emptyUrlMessage: "Capacitor API request URL is required.",
124
+ missingApiBaseUrlMessage: "config.mobile.apiBaseUrl is required for Capacitor API requests.",
125
+ invalidUrlProtocolMessage: "Capacitor API request URL must use http or https."
126
+ });
127
+
128
+ return fetchImpl(normalizedInput.rebuild(resolvedUrl), init);
129
+ };
130
+ }
131
+
132
+ export {
133
+ createCapacitorAwareFetch,
134
+ isCapacitorApiRequestTarget,
135
+ resolveCapacitorAbsoluteHttpUrl
136
+ };
@@ -0,0 +1,136 @@
1
+ function resolveCapacitorAppPlugin(globalObject = globalThis) {
2
+ if (!globalObject || typeof globalObject !== "object") {
3
+ return null;
4
+ }
5
+
6
+ const pluginFromPlugins = globalObject?.Capacitor?.Plugins?.App;
7
+ if (pluginFromPlugins && typeof pluginFromPlugins === "object") {
8
+ return pluginFromPlugins;
9
+ }
10
+
11
+ const directPlugin = globalObject?.Capacitor?.App;
12
+ if (directPlugin && typeof directPlugin === "object") {
13
+ return directPlugin;
14
+ }
15
+
16
+ return null;
17
+ }
18
+
19
+ function createNoopCapacitorAppAdapter() {
20
+ return Object.freeze({
21
+ available: false,
22
+ async getInitialLaunchUrl() {
23
+ return "";
24
+ },
25
+ subscribeToLaunchUrls() {
26
+ return () => {};
27
+ },
28
+ subscribeToBackButton() {
29
+ return () => {};
30
+ },
31
+ async exitApp() {
32
+ return false;
33
+ }
34
+ });
35
+ }
36
+
37
+ function createGlobalCapacitorAppAdapter({ globalObject = globalThis, appPlugin = null } = {}) {
38
+ const plugin = appPlugin || resolveCapacitorAppPlugin(globalObject);
39
+ if (!plugin || typeof plugin !== "object") {
40
+ return createNoopCapacitorAppAdapter();
41
+ }
42
+
43
+ return Object.freeze({
44
+ available: typeof plugin.getLaunchUrl === "function" || typeof plugin.addListener === "function",
45
+ async getInitialLaunchUrl() {
46
+ if (typeof plugin.getLaunchUrl !== "function") {
47
+ return "";
48
+ }
49
+
50
+ const payload = await plugin.getLaunchUrl();
51
+ return String(payload?.url || "").trim();
52
+ },
53
+ subscribeToLaunchUrls(handler = () => {}) {
54
+ if (typeof plugin.addListener !== "function") {
55
+ return () => {};
56
+ }
57
+
58
+ let disposed = false;
59
+ let removeListener = null;
60
+
61
+ Promise.resolve(
62
+ plugin.addListener("appUrlOpen", (event = {}) => {
63
+ if (disposed) {
64
+ return;
65
+ }
66
+
67
+ handler(String(event?.url || "").trim());
68
+ })
69
+ )
70
+ .then((listenerHandle) => {
71
+ removeListener = listenerHandle;
72
+ if (disposed && typeof removeListener?.remove === "function") {
73
+ return removeListener.remove();
74
+ }
75
+ return null;
76
+ })
77
+ .catch(() => {});
78
+
79
+ return () => {
80
+ disposed = true;
81
+ if (typeof removeListener?.remove === "function") {
82
+ void removeListener.remove();
83
+ }
84
+ };
85
+ },
86
+ subscribeToBackButton(handler = () => {}) {
87
+ if (typeof plugin.addListener !== "function") {
88
+ return () => {};
89
+ }
90
+
91
+ let disposed = false;
92
+ let removeListener = null;
93
+
94
+ Promise.resolve(
95
+ plugin.addListener("backButton", (event = {}) => {
96
+ if (disposed) {
97
+ return;
98
+ }
99
+
100
+ handler({
101
+ canGoBack: event?.canGoBack === true
102
+ });
103
+ })
104
+ )
105
+ .then((listenerHandle) => {
106
+ removeListener = listenerHandle;
107
+ if (disposed && typeof removeListener?.remove === "function") {
108
+ return removeListener.remove();
109
+ }
110
+ return null;
111
+ })
112
+ .catch(() => {});
113
+
114
+ return () => {
115
+ disposed = true;
116
+ if (typeof removeListener?.remove === "function") {
117
+ void removeListener.remove();
118
+ }
119
+ };
120
+ },
121
+ async exitApp() {
122
+ if (typeof plugin.exitApp !== "function") {
123
+ return false;
124
+ }
125
+
126
+ await plugin.exitApp();
127
+ return true;
128
+ }
129
+ });
130
+ }
131
+
132
+ export {
133
+ createGlobalCapacitorAppAdapter,
134
+ createNoopCapacitorAppAdapter,
135
+ resolveCapacitorAppPlugin
136
+ };
@@ -0,0 +1,215 @@
1
+ import { registerMobileLaunchRouting, resolveMobileConfig } from "@jskit-ai/kernel/client";
2
+ import { createNoopCapacitorAppAdapter } from "./globalCapacitorAppAdapter.js";
3
+
4
+ function extractPathname(value = "") {
5
+ const normalizedValue = String(value || "").trim();
6
+ if (!normalizedValue) {
7
+ return "";
8
+ }
9
+
10
+ try {
11
+ const parsed = new URL(normalizedValue, "https://jskit.invalid");
12
+ return String(parsed.pathname || "").trim();
13
+ } catch {
14
+ return "";
15
+ }
16
+ }
17
+
18
+ function normalizeCallbackCompleter(value = null) {
19
+ if (value && typeof value.completeFromUrl === "function") {
20
+ return value;
21
+ }
22
+
23
+ if (typeof value === "function") {
24
+ return Object.freeze({
25
+ completeFromUrl: value
26
+ });
27
+ }
28
+
29
+ return null;
30
+ }
31
+
32
+ function normalizeAuthGuardRuntime(value = null) {
33
+ if (
34
+ value &&
35
+ typeof value === "object" &&
36
+ typeof value.getState === "function" &&
37
+ (typeof value.initialize === "function" || typeof value.refresh === "function")
38
+ ) {
39
+ return value;
40
+ }
41
+
42
+ return null;
43
+ }
44
+
45
+ function isAuthCallbackTargetPath(targetPath = "", mobileConfig = {}) {
46
+ const callbackPath = String(mobileConfig?.auth?.callbackPath || "").trim();
47
+ if (!callbackPath) {
48
+ return false;
49
+ }
50
+
51
+ return extractPathname(targetPath) === callbackPath;
52
+ }
53
+
54
+ function createMobileCapacitorRuntime({
55
+ router,
56
+ mobileConfig = {},
57
+ adapter = createNoopCapacitorAppAdapter(),
58
+ placementRuntime = null,
59
+ authCallbackCompleter = null,
60
+ authGuardRuntime = null,
61
+ logger = null
62
+ } = {}) {
63
+ if (!router || typeof router.replace !== "function") {
64
+ throw new TypeError("createMobileCapacitorRuntime requires router.replace().");
65
+ }
66
+
67
+ const resolvedMobileConfig = resolveMobileConfig({
68
+ mobile: mobileConfig
69
+ });
70
+ const resolvedAdapter = adapter && typeof adapter === "object" ? adapter : createNoopCapacitorAppAdapter();
71
+ const resolvedAuthCallbackCompleter = normalizeCallbackCompleter(authCallbackCompleter);
72
+ const resolvedAuthGuardRuntime = normalizeAuthGuardRuntime(authGuardRuntime);
73
+ let launchRouting = null;
74
+ let initialized = false;
75
+ let lastAppliedPath = "";
76
+ let authGuardReadyPromise = null;
77
+ let removeBackButtonListener = null;
78
+
79
+ async function ensureAuthGuardReady() {
80
+ if (!resolvedAuthGuardRuntime) {
81
+ return null;
82
+ }
83
+
84
+ if (authGuardReadyPromise) {
85
+ return authGuardReadyPromise;
86
+ }
87
+
88
+ authGuardReadyPromise = (async () => {
89
+ if (typeof resolvedAuthGuardRuntime.initialize === "function") {
90
+ return resolvedAuthGuardRuntime.initialize();
91
+ }
92
+ return resolvedAuthGuardRuntime.refresh();
93
+ })();
94
+
95
+ try {
96
+ return await authGuardReadyPromise;
97
+ } catch (error) {
98
+ authGuardReadyPromise = null;
99
+ throw error;
100
+ }
101
+ }
102
+
103
+ async function resolveTargetPath({ originalUrl = "", normalizedTargetPath = "" } = {}) {
104
+ if (!resolvedAuthCallbackCompleter || !isAuthCallbackTargetPath(normalizedTargetPath, resolvedMobileConfig)) {
105
+ return normalizedTargetPath;
106
+ }
107
+
108
+ const fallbackReturnTo = String(router.currentRoute?.value?.fullPath || "/").trim() || "/";
109
+ const authResult = await resolvedAuthCallbackCompleter.completeFromUrl({
110
+ url: originalUrl,
111
+ fallbackReturnTo,
112
+ placementContext: placementRuntime && typeof placementRuntime.getContext === "function"
113
+ ? placementRuntime.getContext()
114
+ : null,
115
+ defaultProvider:
116
+ resolvedAuthGuardRuntime && typeof resolvedAuthGuardRuntime.getState === "function"
117
+ ? String(resolvedAuthGuardRuntime.getState()?.oauthDefaultProvider || "")
118
+ : "",
119
+ refreshSession:
120
+ resolvedAuthGuardRuntime && typeof resolvedAuthGuardRuntime.refresh === "function"
121
+ ? () => resolvedAuthGuardRuntime.refresh()
122
+ : async () => null
123
+ });
124
+
125
+ if (authResult?.completed === true) {
126
+ return authResult.returnTo || fallbackReturnTo;
127
+ }
128
+
129
+ return normalizedTargetPath;
130
+ }
131
+
132
+ function createLaunchRouting() {
133
+ return registerMobileLaunchRouting({
134
+ router,
135
+ mobileConfig: resolvedMobileConfig,
136
+ getInitialLaunchUrl: () => resolvedAdapter.getInitialLaunchUrl(),
137
+ subscribeToLaunchUrls: (handler) => resolvedAdapter.subscribeToLaunchUrls(handler),
138
+ resolveTargetPath,
139
+ logger
140
+ });
141
+ }
142
+
143
+ function wireBackButtonHandling() {
144
+ if (typeof resolvedAdapter.subscribeToBackButton !== "function") {
145
+ return;
146
+ }
147
+ if (!router || typeof router.back !== "function") {
148
+ return;
149
+ }
150
+
151
+ removeBackButtonListener = resolvedAdapter.subscribeToBackButton(async (event = {}) => {
152
+ if (event?.canGoBack === true) {
153
+ router.back();
154
+ return;
155
+ }
156
+
157
+ if (typeof resolvedAdapter.exitApp === "function") {
158
+ await resolvedAdapter.exitApp();
159
+ }
160
+ });
161
+ }
162
+
163
+ async function initialize() {
164
+ if (initialized) {
165
+ return lastAppliedPath;
166
+ }
167
+
168
+ initialized = true;
169
+ await ensureAuthGuardReady();
170
+ launchRouting = createLaunchRouting();
171
+ wireBackButtonHandling();
172
+ lastAppliedPath = await launchRouting.initialize();
173
+ return lastAppliedPath;
174
+ }
175
+
176
+ async function applyIncomingUrl(url = "", reason = "manual") {
177
+ if (!launchRouting) {
178
+ await ensureAuthGuardReady();
179
+ launchRouting = createLaunchRouting();
180
+ }
181
+
182
+ lastAppliedPath = await launchRouting.applyIncomingUrl(url, reason);
183
+ return lastAppliedPath;
184
+ }
185
+
186
+ function dispose() {
187
+ if (launchRouting && typeof launchRouting.dispose === "function") {
188
+ launchRouting.dispose();
189
+ }
190
+ if (typeof removeBackButtonListener === "function") {
191
+ removeBackButtonListener();
192
+ }
193
+ launchRouting = null;
194
+ removeBackButtonListener = null;
195
+ initialized = false;
196
+ }
197
+
198
+ function getState() {
199
+ return Object.freeze({
200
+ initialized,
201
+ available: resolvedAdapter.available === true,
202
+ enabled: resolvedMobileConfig.enabled === true,
203
+ lastAppliedPath
204
+ });
205
+ }
206
+
207
+ return Object.freeze({
208
+ initialize,
209
+ applyIncomingUrl,
210
+ dispose,
211
+ getState
212
+ });
213
+ }
214
+
215
+ export { createMobileCapacitorRuntime };
@@ -0,0 +1,45 @@
1
+ import { Browser } from "@capacitor/browser";
2
+ import { resolveCapacitorAbsoluteHttpUrl } from "./apiRequestClient.js";
3
+
4
+ function resolveCapacitorLaunchUrl(url = "", apiBaseUrl = "") {
5
+ return resolveCapacitorAbsoluteHttpUrl(url, apiBaseUrl, {
6
+ emptyUrlMessage: "OAuth launch URL is required.",
7
+ missingApiBaseUrlMessage: "config.mobile.apiBaseUrl is required to launch OAuth from the Capacitor shell.",
8
+ invalidUrlProtocolMessage: "OAuth launch URL must use http or https."
9
+ });
10
+ }
11
+
12
+ function createCapacitorAwareOAuthLaunchClient({
13
+ adapter = null,
14
+ browserPlugin = Browser,
15
+ location = null,
16
+ apiBaseUrl = ""
17
+ } = {}) {
18
+ const resolvedLocation =
19
+ location || (typeof window === "object" && window?.location ? window.location : null);
20
+
21
+ return Object.freeze({
22
+ async open({ url = "" } = {}) {
23
+ const normalizedUrl = String(url || "").trim();
24
+ if (!normalizedUrl) {
25
+ throw new Error("OAuth launch URL is required.");
26
+ }
27
+
28
+ if (adapter?.available === true && browserPlugin && typeof browserPlugin.open === "function") {
29
+ await browserPlugin.open({
30
+ url: resolveCapacitorLaunchUrl(normalizedUrl, apiBaseUrl)
31
+ });
32
+ return true;
33
+ }
34
+
35
+ if (!resolvedLocation || typeof resolvedLocation.assign !== "function") {
36
+ throw new Error("Browser location.assign() is unavailable for OAuth launch.");
37
+ }
38
+
39
+ resolvedLocation.assign(normalizedUrl);
40
+ return true;
41
+ }
42
+ });
43
+ }
44
+
45
+ export { createCapacitorAwareOAuthLaunchClient, resolveCapacitorLaunchUrl };