@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.
- package/.turbo/turbo-build.log +32 -0
- package/.vscode/extensions.json +4 -0
- package/.vscode/launch.json +11 -0
- package/CHANGELOG.md +9 -0
- package/README.md +68 -0
- package/astro.config.mjs +12 -0
- package/cli.js +49 -0
- package/package.json +37 -0
- package/public/favicon.png +0 -0
- package/src/components/README.md +3 -0
- package/src/components/app/app.component.tsx +83 -0
- package/src/components/app/app.context.tsx +41 -0
- package/src/components/app/app.css.ts +15 -0
- package/src/components/app/index.ts +1 -0
- package/src/components/card/card.component.tsx +42 -0
- package/src/components/card/card.css.ts +39 -0
- package/src/components/card/index.ts +1 -0
- package/src/components/flow/flow.component.tsx +89 -0
- package/src/components/flow/flow.domain.ts +18 -0
- package/src/components/flow/flow.error.tsx +32 -0
- package/src/components/flow/flow.initial.tsx +14 -0
- package/src/components/flow/flow.loader.tsx +9 -0
- package/src/components/flow/flow.progress.tsx +94 -0
- package/src/components/flow/flow.success.tsx +39 -0
- package/src/components/flow/flow.types.ts +48 -0
- package/src/components/flow/index.ts +3 -0
- package/src/components/text/index.ts +1 -0
- package/src/components/text/text.component.tsx +8 -0
- package/src/config.ts +7 -0
- package/src/domain/i18n.ts +54 -0
- package/src/env.d.ts +1 -0
- package/src/layouts/README.md +3 -0
- package/src/layouts/page.astro +33 -0
- package/src/pages/index.astro +8 -0
- package/tsconfig.json +7 -0
|
@@ -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
|
+
[36mvite v4.5.0 [32mbuilding for production...[36m[39m
|
|
14
|
+
transforming...
|
|
15
|
+
[32m✓[39m 377 modules transformed.
|
|
16
|
+
rendering chunks...
|
|
17
|
+
computing gzip size...
|
|
18
|
+
[2mdist/[22m[35m_astro/index.e24832a0.css [39m[1m[2m 31.53 kB[22m[1m[22m[2m │ gzip: 5.44 kB[22m
|
|
19
|
+
[2mdist/[22m[36m_astro/index.03be2d59.js [39m[1m[2m 6.83 kB[22m[1m[22m[2m │ gzip: 2.72 kB[22m
|
|
20
|
+
[2mdist/[22m[36m_astro/client.a5ffbec0.js [39m[1m[2m135.34 kB[22m[1m[22m[2m │ gzip: 43.73 kB[22m
|
|
21
|
+
[2mdist/[22m[36m_astro/app.16dd2e70.js [39m[1m[2m166.26 kB[22m[1m[22m[2m │ gzip: 47.25 kB[22m
|
|
22
|
+
[32m✓ built in 3.75s[39m
|
|
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!
|
package/CHANGELOG.md
ADDED
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
|
+
```
|
package/astro.config.mjs
ADDED
|
@@ -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,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,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 @@
|
|
|
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,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,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>
|