@slashid/jump-page 0.0.2

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,32 @@
1
+
2
+ > @slashid/jump-page@0.0.2 build /home/runner/work/javascript/javascript/apps/jump-page
3
+ > astro build
4
+
5
+ 09:03:24 PM [content] No content directory found. Skipping type generation.
6
+ 09:03:24 PM [build] output target: static
7
+ 09:03:24 PM [build] Collecting build info...
8
+ 09:03:24 PM [build] Completed in 371ms.
9
+ 09:03:24 PM [build] Building static entrypoints...
10
+ 09:03:27 PM [build] Completed in 3.23s.
11
+
12
+ building client
13
+ vite v4.5.0 building for production...
14
+ transforming...
15
+ ✓ 377 modules transformed.
16
+ rendering chunks...
17
+ computing gzip size...
18
+ dist/_astro/index.e24832a0.css  31.53 kB │ gzip: 5.44 kB
19
+ dist/_astro/index.03be2d59.js  6.83 kB │ gzip: 2.72 kB
20
+ dist/_astro/client.a5ffbec0.js 135.34 kB │ gzip: 43.73 kB
21
+ dist/_astro/app.16dd2e70.js 166.26 kB │ gzip: 47.25 kB
22
+ ✓ built in 3.75s
23
+ Completed in 3.76s.
24
+
25
+
26
+ generating static routes
27
+ ▶ src/pages/index.astro
28
+ └─ /index.html (+112ms)
29
+ Completed in 124ms.
30
+
31
+ 09:03:31 PM [build] 1 page(s) built in 7.50s
32
+ 09:03:31 PM [build] Complete!
@@ -0,0 +1,4 @@
1
+ {
2
+ "recommendations": ["astro-build.astro-vscode"],
3
+ "unwantedRecommendations": []
4
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "version": "0.2.0",
3
+ "configurations": [
4
+ {
5
+ "command": "./node_modules/.bin/astro dev",
6
+ "name": "Development server",
7
+ "request": "launch",
8
+ "type": "node-terminal"
9
+ }
10
+ ]
11
+ }
package/CHANGELOG.md ADDED
@@ -0,0 +1,9 @@
1
+ # @slashid/jump-page
2
+
3
+ ## 0.0.2
4
+
5
+ ### Patch Changes
6
+
7
+ - c1cf068: Update dependencies
8
+ - Updated dependencies [c1cf068]
9
+ - @slashid/react-primitives@0.1.0
package/README.md ADDED
@@ -0,0 +1,68 @@
1
+ # /id - SDK Jump page
2
+
3
+ WIP jump page implementation for the SDK. SSG + client side rendering.
4
+
5
+ ## Running the app via CLI
6
+
7
+ This package comes with CLI support so you can run it locally:
8
+
9
+ ```bash
10
+ npm install -g @slashid/jump-page
11
+ ```
12
+
13
+ ### CLI docs
14
+
15
+ ```bash
16
+ Usage: sid-jump-cli serve [options]
17
+
18
+ Start the development server
19
+
20
+ Options:
21
+ -p, --port <number> Port to use for the development server (default: 4321)
22
+ -a, --api-url <char> SlashID API URL (default: "https://api.slashid.com")
23
+ -s, --sdk-url <char> SlashID SDK URL (default: "https://cdn.slashid.com/sdk.html")
24
+ -h, --help display help for command
25
+ ```
26
+
27
+ ## Astro gotchas
28
+
29
+ ### React hydration
30
+
31
+ Astro renders a static site by default, meaning the even when using React components they won't hydrate out of the box - it is required to explicitly tell Astro which components should be hydrated by [creating an island](https://docs.astro.build/en/concepts/islands/#creating-an-island).
32
+
33
+ Another gotcha is that when rendering multiple React components from an Astro component, it is not enough to create an island for the parent component, but for each of the children as well. Example:
34
+
35
+ ```jsx
36
+ // Page.astro
37
+ ---
38
+ import { Parent, Child } from './components';
39
+ ---
40
+
41
+ <Parent client:load>
42
+ <Child client:load />
43
+ </Parent>
44
+ ```
45
+
46
+ Notice how the `Child` component also needs the `client:load` attribute. Otherwise it would only be rendered when generating the site.
47
+
48
+ To prevent issues like these, encapsulate the whole client side app in a single parent component and create an island for it.
49
+
50
+ ```jsx
51
+ // app.tsx
52
+ import { Parent, Child } from "./components";
53
+
54
+ export function App() {
55
+ return (
56
+ <Parent>
57
+ <Child client:load />
58
+ </Parent>
59
+ );
60
+ }
61
+
62
+ // Page.astro
63
+ ---
64
+ import { App } from './app';
65
+ ---
66
+
67
+ <App client:load />
68
+ ```
@@ -0,0 +1,12 @@
1
+ import { defineConfig } from "astro/config";
2
+
3
+ import react from "@astrojs/react";
4
+ import { vanillaExtractPlugin } from "@vanilla-extract/vite-plugin";
5
+
6
+ // https://astro.build/config
7
+ export default defineConfig({
8
+ integrations: [react()],
9
+ vite: {
10
+ plugins: [vanillaExtractPlugin()],
11
+ },
12
+ });
package/cli.js ADDED
@@ -0,0 +1,49 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { program } from "commander";
4
+ import { exec } from "child_process";
5
+ import { createRequire } from "node:module";
6
+
7
+ const require = createRequire(import.meta.url);
8
+
9
+ const packageJson = require("./package.json");
10
+
11
+ program
12
+ .name("sid-jump-cli")
13
+ .description("/id CLI - Jump page")
14
+ .version(packageJson.version);
15
+
16
+ program
17
+ .command("serve")
18
+ .description("Start the development server")
19
+ .option("-p, --port <number>", "Port to use for the development server", 4321)
20
+ .option(
21
+ "-a, --api-url <char>",
22
+ "SlashID API URL",
23
+ "https://api.sandbox.slashid.com"
24
+ )
25
+ .option(
26
+ "-s, --sdk-url <char>",
27
+ "SlashID SDK URL",
28
+ "https://cdn.sandbox.slashid.com/sdk.html"
29
+ )
30
+ .action((options) => {
31
+ const command = `PUBLIC_SID_SDK_URL=${options.sdkUrl} PUBLIC_SID_API_URL=${options.apiUrl} npm run dev`;
32
+
33
+ // Set the required env variables and execute the "dev" script defined in package.json
34
+ const childProcess = exec(command);
35
+
36
+ childProcess.stdout.pipe(process.stdout);
37
+ childProcess.stderr.pipe(process.stderr);
38
+
39
+ childProcess.on("error", (error) => {
40
+ console.error(`Error: ${error.message}`);
41
+ });
42
+
43
+ childProcess.on("close", (code) => {
44
+ console.log(`Child process exited with code ${code}`);
45
+ });
46
+ });
47
+
48
+ // Parse the command line arguments
49
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@slashid/jump-page",
3
+ "type": "module",
4
+ "version": "0.0.2",
5
+ "private": false,
6
+ "publishConfig": {
7
+ "access": "restricted"
8
+ },
9
+ "bin": {
10
+ "sid-jump-cli": "./cli.js"
11
+ },
12
+ "dependencies": {
13
+ "@astrojs/check": "^0.3.1",
14
+ "@astrojs/react": "^3.0.5",
15
+ "@sentry/react": "^7.81.1",
16
+ "@slashid/slashid": "3.16.0",
17
+ "@types/react": "^18.0.21",
18
+ "@types/react-dom": "^18.0.6",
19
+ "astro": "^3.5.5",
20
+ "commander": "^11.1.0",
21
+ "react": "^18.0.0",
22
+ "react-dom": "^18.0.0",
23
+ "@slashid/react-primitives": "0.1.0"
24
+ },
25
+ "devDependencies": {
26
+ "@vanilla-extract/css": "^1.9.2",
27
+ "@vanilla-extract/vite-plugin": "^3.9.2"
28
+ },
29
+ "scripts": {
30
+ "dev": "astro dev",
31
+ "start": "astro dev",
32
+ "test": "astro check",
33
+ "build": "astro build",
34
+ "preview": "astro preview",
35
+ "astro": "astro"
36
+ }
37
+ }
Binary file
@@ -0,0 +1,3 @@
1
+ # Components
2
+
3
+ This folder should only contain components meant to render server side and then hydrate on the client side.
@@ -0,0 +1,83 @@
1
+ import { SlashID } from "@slashid/slashid";
2
+ import * as Sentry from "@sentry/react";
3
+ import {
4
+ Logo,
5
+ TextProvider,
6
+ ThemeRoot,
7
+ isBrowser,
8
+ } from "@slashid/react-primitives";
9
+
10
+ import {
11
+ AppContext,
12
+ initialAppContextState,
13
+ type AppState,
14
+ type AppContextState,
15
+ type AppReadyState,
16
+ } from "./app.context";
17
+ import { useEffect, useMemo, useRef, useState } from "react";
18
+ import { I18N, detectLanguage } from "../../domain/i18n";
19
+ import { Flow, Error } from "../flow";
20
+ import { Card } from "../card";
21
+
22
+ import * as styles from "./app.css";
23
+ import { config } from "../../config";
24
+
25
+ export function App() {
26
+ const isClientSide = isBrowser();
27
+ const [appState, setAppState] = useState<AppState>("initial");
28
+ const sidRef = useRef<SlashID | undefined>(undefined);
29
+
30
+ useEffect(() => {
31
+ if (appState === "initial") {
32
+ Sentry.init({
33
+ dsn: "https://6435e8a20ba8b61404fb4621d2aedd4a@o4505317722619904.ingest.sentry.io/4506275681075200",
34
+ });
35
+
36
+ sidRef.current = new SlashID({
37
+ analyticsEnabled: false,
38
+ sdkURL: config.sdkURL,
39
+ baseURL: config.baseURL,
40
+ });
41
+
42
+ setAppState("ready");
43
+ }
44
+ }, [appState]);
45
+
46
+ const state: AppContextState = useMemo(() => {
47
+ // checking for hydration makes the first client side render the same as the one on the server side
48
+ // language check and URL params can only be done on the client side so we need to wait for the first render
49
+ if (!isClientSide || appState == "initial") {
50
+ return initialAppContextState;
51
+ }
52
+
53
+ const appReadyState: AppReadyState = {
54
+ // TODO read customisation props from the URL
55
+ logo: <Logo />,
56
+ language: detectLanguage(),
57
+ state: appState,
58
+ sdk: sidRef.current as SlashID,
59
+ };
60
+
61
+ return appReadyState;
62
+ }, [appState, isClientSide]);
63
+
64
+ return (
65
+ <ThemeRoot theme="light">
66
+ <Sentry.ErrorBoundary
67
+ fallback={
68
+ <Card>
69
+ <Error type="error" />
70
+ </Card>
71
+ }
72
+ >
73
+ <AppContext.Provider value={state}>
74
+ <TextProvider text={I18N[state.language]}>
75
+ <main className={styles.app}>
76
+ <Flow />
77
+ </main>
78
+ </TextProvider>
79
+ </AppContext.Provider>
80
+ </Sentry.ErrorBoundary>
81
+ </ThemeRoot>
82
+ );
83
+ }
@@ -0,0 +1,41 @@
1
+ import { createContext, useContext } from "react";
2
+ import { Logo } from "@slashid/react-primitives";
3
+ import type { SlashID } from "@slashid/slashid";
4
+
5
+ import { type Language } from "../../domain/i18n";
6
+
7
+ export type AppState = "initial" | "ready";
8
+
9
+ type AppConfig = {
10
+ logo: string | React.ReactNode;
11
+ language: Language;
12
+ };
13
+
14
+ export type AppInitialState = AppConfig & {
15
+ state: "initial";
16
+ sdk: undefined;
17
+ };
18
+
19
+ export type AppReadyState = AppConfig & {
20
+ state: "ready";
21
+ sdk: SlashID;
22
+ };
23
+
24
+ export type AppContextState = AppInitialState | AppReadyState;
25
+
26
+ export const initialAppContextState: AppContextState = {
27
+ logo: <Logo />,
28
+ language: "en",
29
+ state: "initial",
30
+ sdk: undefined,
31
+ };
32
+
33
+ export const AppContext = createContext<AppContextState>(
34
+ initialAppContextState
35
+ );
36
+
37
+ export const useAppContext = () => {
38
+ const appContext = useContext(AppContext);
39
+
40
+ return appContext;
41
+ };
@@ -0,0 +1,15 @@
1
+ import { publicVariables } from "@slashid/react-primitives";
2
+ import { style } from "@vanilla-extract/css";
3
+
4
+ export const app = style({
5
+ fontFamily: publicVariables.font.fontFamily,
6
+ backgroundColor: publicVariables.color.background,
7
+ boxSizing: "border-box",
8
+ display: "flex",
9
+ justifyContent: "center",
10
+ alignItems: "center",
11
+ width: "100%",
12
+ height: "100vh",
13
+ padding: 0,
14
+ margin: 0,
15
+ });
@@ -0,0 +1 @@
1
+ export * from "./app.component";
@@ -0,0 +1,42 @@
1
+ import { useAppContext } from "../app/app.context";
2
+ import { Text } from "../text";
3
+ import * as styles from "./card.css";
4
+
5
+ export type Props = {
6
+ children: React.ReactNode;
7
+ footer?: React.ReactNode;
8
+ header?: React.ReactNode;
9
+ };
10
+
11
+ function DefaultHeader() {
12
+ const { logo } = useAppContext();
13
+
14
+ return <div className={styles.logo}>{logo}</div>;
15
+ }
16
+
17
+ function DefaultFooter() {
18
+ return (
19
+ <Text
20
+ t="footer.text"
21
+ variant={{
22
+ size: "xs",
23
+ weight: "semibold",
24
+ color: "placeholder",
25
+ }}
26
+ />
27
+ );
28
+ }
29
+
30
+ export function Card({
31
+ children,
32
+ header = <DefaultHeader />,
33
+ footer = <DefaultFooter />,
34
+ }: Props) {
35
+ return (
36
+ <article className={styles.card}>
37
+ {header && <header className={styles.header}>{header}</header>}
38
+ <div>{children}</div>
39
+ {footer && <footer className={styles.footer}>{footer}</footer>}
40
+ </article>
41
+ );
42
+ }
@@ -0,0 +1,39 @@
1
+ import { publicVariables, theme } from "@slashid/react-primitives";
2
+ import { globalStyle, style } from "@vanilla-extract/css";
3
+
4
+ export const card = style({
5
+ display: "grid",
6
+ gridTemplateRows: "auto 1fr auto",
7
+ backgroundColor: publicVariables.color.panel,
8
+ width: "100%",
9
+ minHeight: "100vh",
10
+ padding: "32px 32px 16px 32px",
11
+ boxSizing: "border-box",
12
+
13
+ "@media": {
14
+ "screen and (min-width: 768px)": {
15
+ borderRadius: `calc(2 * ${publicVariables.border.radius})`,
16
+ width: "465px",
17
+ minHeight: "330px",
18
+ },
19
+ },
20
+ });
21
+
22
+ export const header = style({
23
+ marginBottom: theme.space[1],
24
+ });
25
+
26
+ export const footer = style({
27
+ width: "100%",
28
+ textAlign: "center",
29
+ });
30
+
31
+ export const logo = style({
32
+ width: 32,
33
+ height: 32,
34
+ });
35
+
36
+ globalStyle(`${logo} > svg`, {
37
+ height: 32,
38
+ width: 32,
39
+ });
@@ -0,0 +1 @@
1
+ export * from "./card.component";
@@ -0,0 +1,89 @@
1
+ import { useEffect, useState } from "react";
2
+ import * as Sentry from "@sentry/react";
3
+ import { Card } from "../card";
4
+ import { Initial } from "./flow.initial";
5
+ import { Error } from "./flow.error";
6
+ import type { Challenges, State, FlowType } from "./flow.types";
7
+ import { useAppContext } from "../app/app.context";
8
+ import { Progress } from "./flow.progress";
9
+ import type { ChallengeListInner } from "@slashid/slashid";
10
+ import { Success } from "./flow.success";
11
+ import { ensureError } from "./flow.domain";
12
+
13
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
14
+ function getFlowTypeFromChallenges(challenges: Challenges): FlowType {
15
+ // TODO treat passwords as a special case
16
+ return "catch-all";
17
+ }
18
+
19
+ export function Flow() {
20
+ const [flowState, setFlowState] = useState<State>({ state: "initial" });
21
+ const { sdk, state: appState } = useAppContext();
22
+
23
+ useEffect(() => {
24
+ if (flowState.state !== "initial") return;
25
+
26
+ async function processChallenges() {
27
+ if (!sdk) return;
28
+ setFlowState({ state: "parsing-url" });
29
+
30
+ let challenges: ChallengeListInner[] | null = [];
31
+
32
+ try {
33
+ challenges = await sdk.getChallengesFromURL();
34
+ } catch (e: unknown) {
35
+ Sentry.captureException(ensureError(e));
36
+ }
37
+
38
+ if (!challenges) {
39
+ setFlowState({ state: "no-challenges", challenges: null });
40
+ return;
41
+ }
42
+
43
+ const flowType = getFlowTypeFromChallenges(challenges);
44
+
45
+ setFlowState({ state: "progress", challenges, flowType });
46
+ }
47
+
48
+ processChallenges();
49
+ }, [appState, flowState.state, sdk]);
50
+
51
+ const handleSuccess = () => {
52
+ if (flowState.state !== "progress") return;
53
+
54
+ setFlowState({
55
+ state: "success",
56
+ challenges: flowState.challenges,
57
+ flowType: flowState.flowType,
58
+ });
59
+ };
60
+
61
+ const handleError = ({ error }: { error: Error }) => {
62
+ if (flowState.state !== "progress") return;
63
+
64
+ setFlowState({
65
+ state: "error",
66
+ challenges: flowState.challenges,
67
+ flowType: flowState.flowType,
68
+ error,
69
+ });
70
+ };
71
+
72
+ return (
73
+ <Card>
74
+ {(appState !== "ready" || flowState.state === "parsing-url") && (
75
+ <Initial />
76
+ )}
77
+ {appState === "ready" && (
78
+ <>
79
+ {flowState.state === "progress" && (
80
+ <Progress onSuccess={handleSuccess} onError={handleError} />
81
+ )}
82
+ {flowState.state === "no-challenges" && <Error type="warning" />}
83
+ {flowState.state === "success" && <Success />}
84
+ {flowState.state === "error" && <Error type="error" />}
85
+ </>
86
+ )}
87
+ </Card>
88
+ );
89
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Ensure that a valid Error instance is returned, since in JS you can throw anything.
3
+ */
4
+ export function ensureError(value: unknown): Error {
5
+ if (value instanceof Error) return value;
6
+
7
+ let stringified = "[Unable to stringify the thrown value]";
8
+ try {
9
+ stringified = JSON.stringify(value);
10
+ } catch {
11
+ // ignore
12
+ }
13
+
14
+ const error = new Error(
15
+ `This value was thrown as is, not through an Error: ${stringified}`
16
+ );
17
+ return error;
18
+ }
@@ -0,0 +1,32 @@
1
+ import { Circle, Exclamation, Stack } from "@slashid/react-primitives";
2
+ import { Text } from "../text";
3
+
4
+ export type ErrorType = "warning" | "error";
5
+
6
+ function ErrorIcon({ type }: { type: ErrorType }) {
7
+ return (
8
+ <Circle variant={type === "warning" ? "blue" : "red"} shouldAnimate={false}>
9
+ <Exclamation />
10
+ </Circle>
11
+ );
12
+ }
13
+
14
+ export function Error({ type }: { type: ErrorType }) {
15
+ return (
16
+ <>
17
+ <Stack space="0.25">
18
+ <Text
19
+ as="h1"
20
+ variant={{ size: "2xl-title", weight: "bold" }}
21
+ t="error.title"
22
+ />
23
+ <Text
24
+ as="h2"
25
+ t="error.detail"
26
+ variant={{ color: "contrast", weight: "semibold" }}
27
+ />
28
+ </Stack>
29
+ <ErrorIcon type={type} />
30
+ </>
31
+ );
32
+ }
@@ -0,0 +1,14 @@
1
+ import { Skeleton, Stack } from "@slashid/react-primitives";
2
+ import { Loader } from "./flow.loader";
3
+
4
+ export function Initial() {
5
+ return (
6
+ <>
7
+ <Stack space="2">
8
+ <Skeleton width={"75%"} height={24} />
9
+ <Skeleton width={"50%"} height={16} />
10
+ </Stack>
11
+ <Loader />
12
+ </>
13
+ );
14
+ }
@@ -0,0 +1,9 @@
1
+ import { Circle, Spinner } from "@slashid/react-primitives";
2
+
3
+ export function Loader() {
4
+ return (
5
+ <Circle>
6
+ <Spinner />
7
+ </Circle>
8
+ );
9
+ }
@@ -0,0 +1,94 @@
1
+ import { Stack } from "@slashid/react-primitives";
2
+ import * as Sentry from "@sentry/react";
3
+ import { Loader } from "./flow.loader";
4
+ import { Text } from "../text";
5
+ import { useEffect } from "react";
6
+ import { useAppContext } from "../app/app.context";
7
+
8
+ type SlashIDErrorMessage = { message: string };
9
+ class SlashIDError extends Error {
10
+ errors: SlashIDErrorMessage[];
11
+
12
+ constructor(errors: SlashIDErrorMessage[]) {
13
+ super(errors.length > 0 ? errors[0].message : "Unknown SlashID error");
14
+ this.errors = errors;
15
+ }
16
+ }
17
+
18
+ function isSlashIDError(e: unknown): e is SlashIDError {
19
+ if (
20
+ typeof e === "object" &&
21
+ e !== null &&
22
+ "errors" in e &&
23
+ Array.isArray(e.errors)
24
+ ) {
25
+ return true;
26
+ }
27
+
28
+ return false;
29
+ }
30
+
31
+ function ensureError(value: unknown): Error {
32
+ if (value instanceof Error) return value;
33
+
34
+ // special case - sometimes the core SDK throws a non-error object
35
+ if (isSlashIDError(value)) {
36
+ const error = new SlashIDError(value.errors);
37
+ return error;
38
+ }
39
+
40
+ let stringified = "[Unable to stringify the thrown value]";
41
+ try {
42
+ stringified = JSON.stringify(value);
43
+ } catch {
44
+ // ignore
45
+ }
46
+
47
+ const error = new Error(
48
+ `This value was thrown as is, not through an Error: ${stringified}`
49
+ );
50
+ return error;
51
+ }
52
+
53
+ export type Props = {
54
+ onSuccess: () => void;
55
+ onError: ({ error }: { error: Error }) => void;
56
+ };
57
+
58
+ export function Progress({ onSuccess, onError }: Props) {
59
+ const { sdk } = useAppContext();
60
+ useEffect(() => {
61
+ if (!sdk) return;
62
+
63
+ async function authenticate() {
64
+ try {
65
+ await sdk?.getUserFromURL();
66
+ onSuccess();
67
+ } catch (e: unknown) {
68
+ const safeError = ensureError(e);
69
+ Sentry.captureException(safeError);
70
+ onError({ error: safeError });
71
+ }
72
+ }
73
+
74
+ authenticate();
75
+ }, [onError, onSuccess, sdk]);
76
+
77
+ return (
78
+ <>
79
+ <Stack space="0.25">
80
+ <Text
81
+ as="h1"
82
+ variant={{ size: "2xl-title", weight: "bold" }}
83
+ t="initial.title"
84
+ />
85
+ <Text
86
+ variant={{ color: "contrast", weight: "semibold" }}
87
+ as="h2"
88
+ t="initial.details"
89
+ />
90
+ </Stack>
91
+ <Loader />
92
+ </>
93
+ );
94
+ }
@@ -0,0 +1,39 @@
1
+ import { Circle } from "@slashid/react-primitives";
2
+ import { Text } from "../text";
3
+
4
+ const CheckIcon = () => (
5
+ <Circle>
6
+ <svg
7
+ width="21"
8
+ height="18"
9
+ viewBox="0 0 21 18"
10
+ fill="none"
11
+ xmlns="http://www.w3.org/2000/svg"
12
+ >
13
+ <path
14
+ fillRule="evenodd"
15
+ clipRule="evenodd"
16
+ d="M19.8505 0.705985C20.6342 1.38283 20.7209 2.56684 20.044 3.35055L8.16908 17.1005C7.80192 17.5256 7.2635 17.7637 6.70195 17.7493C6.1404 17.7349 5.61489 17.4695 5.27002 17.0261L0.895049 11.4011C0.259296 10.5837 0.406547 9.4057 1.22394 8.76995C2.04134 8.13419 3.21935 8.28145 3.8551 9.09884L6.82592 12.9185L17.2059 0.89949C17.8828 0.115778 19.0668 0.0291434 19.8505 0.705985Z"
17
+ fill="white"
18
+ />
19
+ </svg>
20
+ </Circle>
21
+ );
22
+
23
+ export function Success() {
24
+ return (
25
+ <article data-testid="sid-form-success-state">
26
+ <Text
27
+ as="h1"
28
+ t="success.title"
29
+ variant={{ size: "2xl-title", weight: "bold" }}
30
+ />
31
+ <Text
32
+ as="h2"
33
+ t="success.details"
34
+ variant={{ color: "contrast", weight: "semibold" }}
35
+ />
36
+ <CheckIcon />
37
+ </article>
38
+ );
39
+ }
@@ -0,0 +1,48 @@
1
+ import type { SlashID } from "@slashid/slashid";
2
+
3
+ export type InitialState = {
4
+ state: "initial";
5
+ };
6
+
7
+ export type ParsingURLState = {
8
+ state: "parsing-url";
9
+ };
10
+
11
+ export type NoChallengesState = {
12
+ state: "no-challenges";
13
+ challenges: null;
14
+ };
15
+
16
+ export type ProgressState = {
17
+ state: "progress";
18
+ challenges: Challenges;
19
+ flowType: FlowType;
20
+ };
21
+
22
+ export type SuccessState = {
23
+ state: "success";
24
+ challenges: Challenges;
25
+ flowType: FlowType;
26
+ };
27
+
28
+ export type ErrorState = {
29
+ state: "error";
30
+ error: Error;
31
+ challenges: Challenges;
32
+ flowType: FlowType;
33
+ };
34
+
35
+ export type State =
36
+ | InitialState
37
+ | ParsingURLState
38
+ | NoChallengesState
39
+ | ProgressState
40
+ | SuccessState
41
+ | ErrorState;
42
+
43
+ export type FlowType = "catch-all";
44
+
45
+ export type ChallengesInURL = Awaited<
46
+ ReturnType<SlashID["getChallengesFromURL"]>
47
+ >;
48
+ export type Challenges = NonNullable<ChallengesInURL>;
@@ -0,0 +1,3 @@
1
+ export * from "./flow.component";
2
+ export * from "./flow.types";
3
+ export * from "./flow.error";
@@ -0,0 +1 @@
1
+ export * from "./text.component";
@@ -0,0 +1,8 @@
1
+ import { Text as BaseText, type TextProps } from "@slashid/react-primitives";
2
+ import type { TranslationKeys } from "../../domain/i18n";
3
+
4
+ type Props = TextProps<Record<TranslationKeys, string>>;
5
+
6
+ export const Text: React.FC<Props> = (props) => {
7
+ return <BaseText {...props} />;
8
+ };
package/src/config.ts ADDED
@@ -0,0 +1,7 @@
1
+ export const config = {
2
+ sdkURL:
3
+ import.meta.env.PUBLIC_SID_SDK_URL ||
4
+ "https://cdn.sandbox.slashid.com/sdk.html",
5
+ baseURL:
6
+ import.meta.env.PUBLIC_SID_API_URL || "https://api.sandbox.slashid.com",
7
+ };
@@ -0,0 +1,54 @@
1
+ // based on RFC 5646
2
+ export type Language = "en" | "ja";
3
+
4
+ /**
5
+ * Check the language based on the browser settings.
6
+ * Default to "en"
7
+ */
8
+ export function detectLanguage(): Language {
9
+ const language = navigator.language.toLowerCase();
10
+ if (language === "ja" || language.startsWith("ja-")) {
11
+ return "ja";
12
+ }
13
+
14
+ return "en";
15
+ }
16
+
17
+ export const defaultStrings = {
18
+ "footer.text": "Top-tier security by SlashID",
19
+ "initial.title": "Logging you in...",
20
+ "initial.details": "Please follow the on-screen instructions.",
21
+ "success.title": "Thank you, you're signed in!",
22
+ "success.details": "You can now close this page.",
23
+ "error.title": "Something went wrong...",
24
+ "error.detail": "Please log in again.",
25
+ };
26
+
27
+ export type TranslationKeys = keyof typeof defaultStrings;
28
+ export type Translations = {
29
+ [key in Language]: {
30
+ [key in TranslationKeys]: string;
31
+ };
32
+ };
33
+
34
+ export const I18N: Translations = {
35
+ en: defaultStrings,
36
+ ja: {
37
+ "footer.text": defaultStrings["footer.text"],
38
+ "initial.title": "ログイン処理を行っています。",
39
+ "initial.details": "処理完了後、画面は自動で切り替わります。",
40
+ "success.title": "認証が完了しました!",
41
+ "success.details": "このページを閉じて、元のページへ戻ってください。",
42
+ "error.title": defaultStrings["error.title"],
43
+ "error.detail": "再度ログインをしてください。",
44
+ },
45
+ } as const;
46
+
47
+ export type TranslationKey = keyof Translations[Language];
48
+
49
+ export type Translate = (
50
+ key: TranslationKey
51
+ ) => (typeof I18N)[Language][TranslationKey];
52
+
53
+ export const createI18n = (language: Language) => (key: TranslationKey) =>
54
+ I18N[language][key];
package/src/env.d.ts ADDED
@@ -0,0 +1 @@
1
+ /// <reference types="astro/client" />
@@ -0,0 +1,3 @@
1
+ # Layouts
2
+
3
+ This folder should only contain [Astro layouts](https://docs.astro.build/en/core-concepts/layouts/).
@@ -0,0 +1,33 @@
1
+ ---
2
+ interface Props {
3
+ title: string;
4
+ }
5
+
6
+ const { title } = Astro.props;
7
+ ---
8
+
9
+ <!DOCTYPE html>
10
+ <html>
11
+ <head>
12
+ <meta charset="utf-8" />
13
+ <meta content="width=device-width, initial-scale=1" name="viewport" />
14
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
15
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
16
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500&display=swap" rel="stylesheet" />
17
+ <link rel="icon" href="/favicon.png">
18
+ <title>{title}</title>
19
+ </head>
20
+
21
+ <body>
22
+ <slot />
23
+ </body>
24
+ </html>
25
+
26
+ <style>
27
+ body {
28
+ width: 100%;
29
+ height: 100vh;
30
+ margin: 0;
31
+ padding: 0;
32
+ }
33
+ </style>
@@ -0,0 +1,8 @@
1
+ ---
2
+ import Page from "../layouts/page.astro";
3
+ import { App } from "../components/app";
4
+ ---
5
+
6
+ <Page title="/id | Jump page">
7
+ <App client:load/>
8
+ </Page>
package/tsconfig.json ADDED
@@ -0,0 +1,7 @@
1
+ {
2
+ "extends": "astro/tsconfigs/strict",
3
+ "compilerOptions": {
4
+ "jsx": "react-jsx",
5
+ "jsxImportSource": "react"
6
+ }
7
+ }