@protobase/ui 0.40.8 → 0.40.9

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/dist/app/app.d.ts CHANGED
@@ -2,6 +2,7 @@ import { SidebarMode } from "../app-shell/sidebar-mode.js";
2
2
  import "../app-shell/index.js";
3
3
  import { ProjectUi } from "./pages/project-ui.js";
4
4
  import { PluginLoader } from "../plugins/load-plugin.js";
5
+ import { NativeBridge } from "./native-auth-fetch.js";
5
6
  import { Locale } from "@protobase/schema/internal";
6
7
  import { AssistantClient, AuthSession, Client, RuntimeClient } from "@protobase/client";
7
8
  //#region src/app/app.d.ts
@@ -30,11 +31,13 @@ export type AppProps = {
30
31
  pluginLoader?: PluginLoader;
31
32
  /** The language the screens are in when the app's `/meta` sets none, such as the browser's (`browserLocale()`); English by default. */
32
33
  locale?: Locale;
34
+ /** The native bridge of the iOS or Android app (`nativeBridgeOf`): sign-in goes through native HTTP. */
35
+ nativeBridge?: NativeBridge;
33
36
  };
34
37
  /**
35
38
  * The admin: a public form at `/-/forms/<name>` for anyone, the sign-in route first, `/-/sign-in`, or browsing as a
36
39
  * visitor when the project's anonymous role reads, then a sidebar from `/meta`, `/:page` composed pages, `/:resource`
37
40
  * lists and `/:resource/:key` records. Fills its parent, in the theme this browser chose, light, dark or the system's.
38
41
  */
39
- export declare const App: ({ auth, workspace, basePath, initialUrl, pluginLoader, locale, ...rest }: AppProps) => import("react").JSX.Element;
42
+ export declare const App: ({ auth, workspace, basePath, initialUrl, pluginLoader, locale, nativeBridge, ...rest }: AppProps) => import("react").JSX.Element;
40
43
  //#endregion
package/dist/app/app.js CHANGED
@@ -328,11 +328,12 @@ const PublicFormGate = ({ baseUrl, client, ui, children }) => {
328
328
  * visitor when the project's anonymous role reads, then a sidebar from `/meta`, `/:page` composed pages, `/:resource`
329
329
  * lists and `/:resource/:key` records. Fills its parent, in the theme this browser chose, light, dark or the system's.
330
330
  */
331
- const App = ({ auth, workspace, basePath, initialUrl, pluginLoader, locale, ...rest }) => {
331
+ const App = ({ auth, workspace, basePath, initialUrl, pluginLoader, locale, nativeBridge, ...rest }) => {
332
332
  setFallbackLocale(locale ?? "en");
333
333
  return /* @__PURE__ */ jsx(ThemeProvider, { children: /* @__PURE__ */ jsx(AuthProvider, {
334
334
  session: auth,
335
335
  ...rest.baseUrl && { origin: new URL(rest.baseUrl, window.location.href).origin },
336
+ ...nativeBridge && { nativeBridge },
336
337
  children: /* @__PURE__ */ jsx(Router, {
337
338
  basePath,
338
339
  initialUrl,
package/dist/app/main.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { browserLocale } from "../locale/active-locale.js";
2
+ import { nativeBridgeOf } from "./native-auth-fetch.js";
2
3
  import { uiFromModule } from "./pages/ui-from-module.js";
3
4
  import { App } from "./app.js";
4
5
  import { pageApiOrigin } from "./page-api-origin.js";
@@ -13,6 +14,7 @@ if (!root) throw new Error("index.html needs an element with id \"root\"");
13
14
  createRoot(root).render(/* @__PURE__ */ jsx(StrictMode, { children: /* @__PURE__ */ jsx(App, {
14
15
  baseUrl: `${pageApiOrigin() ?? ""}/api/v1`,
15
16
  ui: uiFromModule(projectUi),
16
- locale: browserLocale()
17
+ locale: browserLocale(),
18
+ nativeBridge: nativeBridgeOf(window)
17
19
  }) }));
18
20
  //#endregion
@@ -0,0 +1,8 @@
1
+ //#region src/app/native-auth-fetch.d.ts
2
+ /** What the iOS and Android app's native bridge, `window.Capacitor`, offers the page; the shell never imports Capacitor. */
3
+ export type NativeBridge = {
4
+ isNativePlatform(): boolean;
5
+ getPlatform(): string;
6
+ nativePromise(plugin: string, method: string, options: object): Promise<unknown>;
7
+ };
8
+ //#endregion
@@ -0,0 +1,53 @@
1
+ import { authBasePath, capacitorOrigin } from "@protobase/schema/internal";
2
+ //#region src/app/native-auth-fetch.ts
3
+ /** The native bridge of the page, when it runs in the iOS or Android app; `undefined` in a browser. */
4
+ const nativeBridgeOf = (win) => win.Capacitor?.isNativePlatform() ? win.Capacitor : void 0;
5
+ const nullBodyStatuses = [
6
+ 101,
7
+ 204,
8
+ 205,
9
+ 304
10
+ ];
11
+ const encodedSeparator = /%2f|%5c/i;
12
+ const isPlatform = (platform) => Object.hasOwn(capacitorOrigin, platform);
13
+ const allowedUrl = (url, apiOrigin) => url.origin === apiOrigin && !url.username && !url.password && url.pathname.startsWith(`${authBasePath}/`) && !encodedSeparator.test(url.pathname);
14
+ const bodyOf = ({ status, headers, data }) => {
15
+ if (nullBodyStatuses.includes(status)) return null;
16
+ if (Object.entries(headers).some(([name, value]) => name.toLowerCase() === "content-type" && value.startsWith("application/json"))) return JSON.stringify(data);
17
+ return typeof data === "string" ? data : JSON.stringify(data);
18
+ };
19
+ /**
20
+ * A `fetch` for the app's sign-in, at `<apiOrigin>/api/auth/...` only, through Capacitor's native HTTP: the session
21
+ * cookie lives in the device's cookie jar, which native HTTP sends and keeps, and the request names the Capacitor
22
+ * origin of the platform, which the server trusts for sign-in with a mobile section. The page cannot set `Cookie`, so
23
+ * the cookie comes from the native jar alone. Any other address is refused before the bridge is called.
24
+ */
25
+ const nativeAuthFetch = (bridge, apiOrigin) => {
26
+ const platform = bridge.getPlatform();
27
+ if (!isPlatform(platform)) throw new Error(`Signing in through native HTTP is for iOS and Android, not ${platform}`);
28
+ const origin = capacitorOrigin[platform];
29
+ return async (input, init) => {
30
+ const request = new Request(input, init);
31
+ const url = new URL(request.url);
32
+ if (!allowedUrl(url, apiOrigin)) throw new Error(`Native sign-in requests go to ${apiOrigin}${authBasePath}/ only, not ${request.url}`);
33
+ const withBody = request.method !== "GET" && request.method !== "HEAD";
34
+ const answer = await bridge.nativePromise("CapacitorHttp", "request", {
35
+ url: url.href,
36
+ method: request.method,
37
+ headers: {
38
+ ...Object.fromEntries(request.headers),
39
+ origin
40
+ },
41
+ ...withBody && { data: await request.text() },
42
+ responseType: "text",
43
+ disableRedirects: true
44
+ });
45
+ const headers = Object.entries(answer.headers).filter(([name]) => name.toLowerCase() !== "set-cookie");
46
+ return new Response(bodyOf(answer), {
47
+ status: answer.status,
48
+ headers
49
+ });
50
+ };
51
+ };
52
+ //#endregion
53
+ export { nativeAuthFetch, nativeBridgeOf };
@@ -1,3 +1,4 @@
1
+ import { nativeAuthFetch } from "../app/native-auth-fetch.js";
1
2
  import { providerName, withoutSignInLink } from "./provider-link.js";
2
3
  import { text } from "./messages.js";
3
4
  import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
@@ -12,9 +13,10 @@ const useAuth = () => {
12
13
  };
13
14
  /**
14
15
  * Finds out whether the server has a login and who is signed in, and keeps that state for the app. Without a `session`,
15
- * it makes one on `origin`, the page's own by default.
16
+ * it makes one on `origin`, the page's own by default; in the iOS or Android app, with its `nativeBridge`, the session
17
+ * signs in through native HTTP, and "Forgot password?" is not offered, as a reset link cannot come back to the app.
16
18
  */
17
- const AuthProvider = ({ session: given, origin, children }) => {
19
+ const AuthProvider = ({ session: given, origin, nativeBridge, children }) => {
18
20
  const [state, setState] = useState({ kind: "loading" });
19
21
  const [signInMethods, setSignInMethods] = useState(["password"]);
20
22
  const [passwordReset, setPasswordReset] = useState(false);
@@ -35,8 +37,13 @@ const AuthProvider = ({ session: given, origin, children }) => {
35
37
  signedOut.current = markSignedOut;
36
38
  const session = useMemo(() => given ?? createAuthSession({
37
39
  ...origin && { origin },
40
+ ...nativeBridge && { fetch: nativeAuthFetch(nativeBridge, origin ?? window.location.origin) },
38
41
  onSignedOut: () => signedOut.current()
39
- }), [given, origin]);
42
+ }), [
43
+ given,
44
+ origin,
45
+ nativeBridge
46
+ ]);
40
47
  const enter = useCallback(async (user) => {
41
48
  const [{ missing }, staff] = await Promise.all([session.account.signInMethods(), session.staff.current()]);
42
49
  setState(missing.length > 0 ? {
@@ -57,7 +64,7 @@ const AuthProvider = ({ session: given, origin, children }) => {
57
64
  setState({ kind: "loading" });
58
65
  session.status().then(async (status) => {
59
66
  setSignInMethods(status.signInMethods);
60
- setPasswordReset(status.passwordReset);
67
+ setPasswordReset(status.passwordReset && !nativeBridge);
61
68
  setMcp(Boolean(status.mcp));
62
69
  setCanSignUp(Boolean(status.signUp));
63
70
  setPasskeyDomains(status.passkeyDomains ?? []);
@@ -76,7 +83,11 @@ const AuthProvider = ({ session: given, origin, children }) => {
76
83
  kind: "error",
77
84
  message: error instanceof Error ? error.message : text("noAnswer")
78
85
  }));
79
- }, [session, enter]);
86
+ }, [
87
+ session,
88
+ enter,
89
+ nativeBridge
90
+ ]);
80
91
  useEffect(recheck, [recheck]);
81
92
  const api = useMemo(() => ({
82
93
  state,
package/dist/package.js CHANGED
@@ -1,5 +1,5 @@
1
1
  //#region package.json
2
- var version = "0.40.8";
2
+ var version = "0.40.9";
3
3
  var protobase = { "pluginApi": 1 };
4
4
  //#endregion
5
5
  export { protobase, version };