@multiplatform.one/web3 5.0.28 → 5.1.0
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/cjs/connectkit/components/Common/Button/index.cjs +1 -0
- package/dist/cjs/connectkit/components/Common/Button/index.native.js +3 -0
- package/dist/cjs/connectkit/components/Common/Button/index.native.js.map +1 -1
- package/dist/cjs/connectkit/components/Common/Modal/index.cjs +3 -0
- package/dist/cjs/connectkit/components/Common/Modal/index.native.js +3 -0
- package/dist/cjs/connectkit/components/Common/Modal/index.native.js.map +1 -1
- package/dist/cjs/connectkit/components/ConnectKit.cjs +4 -1
- package/dist/cjs/connectkit/components/ConnectKit.native.js +6 -3
- package/dist/cjs/connectkit/components/ConnectKit.native.js.map +1 -1
- package/dist/cjs/connectkit/components/ConnectModal/index.cjs +8 -3
- package/dist/cjs/connectkit/components/ConnectModal/index.native.js +8 -3
- package/dist/cjs/connectkit/components/ConnectModal/index.native.js.map +1 -1
- package/dist/cjs/connectkit/components/Pages/SignInWithGpg/index.cjs +146 -0
- package/dist/cjs/connectkit/components/Pages/SignInWithGpg/index.native.js +162 -0
- package/dist/cjs/connectkit/components/Pages/SignInWithGpg/index.native.js.map +1 -0
- package/dist/cjs/connectkit/components/Standard/GPG/index.cjs +69 -0
- package/dist/cjs/connectkit/components/Standard/GPG/index.native.js +80 -0
- package/dist/cjs/connectkit/components/Standard/GPG/index.native.js.map +1 -0
- package/dist/cjs/connectkit/gpg/GpgContext.cjs +37 -0
- package/dist/cjs/connectkit/gpg/GpgContext.native.js +40 -0
- package/dist/cjs/connectkit/gpg/GpgContext.native.js.map +1 -0
- package/dist/cjs/connectkit/gpg/GpgProvider.cjs +112 -0
- package/dist/cjs/connectkit/gpg/GpgProvider.native.js +127 -0
- package/dist/cjs/connectkit/gpg/GpgProvider.native.js.map +1 -0
- package/dist/cjs/connectkit/gpg/index.cjs +34 -0
- package/dist/cjs/connectkit/gpg/index.native.js +37 -0
- package/dist/cjs/connectkit/gpg/index.native.js.map +1 -0
- package/dist/cjs/connectkit/gpg/useGpg.cjs +70 -0
- package/dist/cjs/connectkit/gpg/useGpg.native.js +77 -0
- package/dist/cjs/connectkit/gpg/useGpg.native.js.map +1 -0
- package/dist/cjs/connectkit/hooks/useModal.cjs +7 -3
- package/dist/cjs/connectkit/hooks/useModal.native.js +8 -2
- package/dist/cjs/connectkit/hooks/useModal.native.js.map +1 -1
- package/dist/cjs/connectkit/index.cjs +7 -0
- package/dist/cjs/connectkit/index.native.js +7 -0
- package/dist/cjs/connectkit/index.native.js.map +1 -1
- package/dist/esm/connectkit/components/Common/Button/index.mjs +1 -0
- package/dist/esm/connectkit/components/Common/Button/index.mjs.map +1 -1
- package/dist/esm/connectkit/components/Common/Button/index.native.js +3 -0
- package/dist/esm/connectkit/components/Common/Button/index.native.js.map +1 -1
- package/dist/esm/connectkit/components/Common/Modal/index.mjs +3 -0
- package/dist/esm/connectkit/components/Common/Modal/index.mjs.map +1 -1
- package/dist/esm/connectkit/components/Common/Modal/index.native.js +3 -0
- package/dist/esm/connectkit/components/Common/Modal/index.native.js.map +1 -1
- package/dist/esm/connectkit/components/ConnectKit.mjs +4 -1
- package/dist/esm/connectkit/components/ConnectKit.mjs.map +1 -1
- package/dist/esm/connectkit/components/ConnectKit.native.js +6 -3
- package/dist/esm/connectkit/components/ConnectKit.native.js.map +1 -1
- package/dist/esm/connectkit/components/ConnectModal/index.mjs +8 -3
- package/dist/esm/connectkit/components/ConnectModal/index.mjs.map +1 -1
- package/dist/esm/connectkit/components/ConnectModal/index.native.js +8 -3
- package/dist/esm/connectkit/components/ConnectModal/index.native.js.map +1 -1
- package/dist/esm/connectkit/components/Pages/SignInWithGpg/index.mjs +110 -0
- package/dist/esm/connectkit/components/Pages/SignInWithGpg/index.mjs.map +1 -0
- package/dist/esm/connectkit/components/Pages/SignInWithGpg/index.native.js +123 -0
- package/dist/esm/connectkit/components/Pages/SignInWithGpg/index.native.js.map +1 -0
- package/dist/esm/connectkit/components/Standard/GPG/index.mjs +32 -0
- package/dist/esm/connectkit/components/Standard/GPG/index.mjs.map +1 -0
- package/dist/esm/connectkit/components/Standard/GPG/index.native.js +40 -0
- package/dist/esm/connectkit/components/Standard/GPG/index.native.js.map +1 -0
- package/dist/esm/connectkit/gpg/GpgContext.mjs +11 -0
- package/dist/esm/connectkit/gpg/GpgContext.mjs.map +1 -0
- package/dist/esm/connectkit/gpg/GpgContext.native.js +11 -0
- package/dist/esm/connectkit/gpg/GpgContext.native.js.map +1 -0
- package/dist/esm/connectkit/gpg/GpgProvider.mjs +86 -0
- package/dist/esm/connectkit/gpg/GpgProvider.mjs.map +1 -0
- package/dist/esm/connectkit/gpg/GpgProvider.native.js +98 -0
- package/dist/esm/connectkit/gpg/GpgProvider.native.js.map +1 -0
- package/dist/esm/connectkit/gpg/index.mjs +5 -0
- package/dist/esm/connectkit/gpg/index.mjs.map +1 -0
- package/dist/esm/connectkit/gpg/index.native.js +5 -0
- package/dist/esm/connectkit/gpg/index.native.js.map +1 -0
- package/dist/esm/connectkit/gpg/useGpg.mjs +45 -0
- package/dist/esm/connectkit/gpg/useGpg.mjs.map +1 -0
- package/dist/esm/connectkit/gpg/useGpg.native.js +49 -0
- package/dist/esm/connectkit/gpg/useGpg.native.js.map +1 -0
- package/dist/esm/connectkit/hooks/useModal.mjs +7 -3
- package/dist/esm/connectkit/hooks/useModal.mjs.map +1 -1
- package/dist/esm/connectkit/hooks/useModal.native.js +8 -2
- package/dist/esm/connectkit/hooks/useModal.native.js.map +1 -1
- package/dist/esm/connectkit/index.mjs +7 -5
- package/dist/esm/connectkit/index.mjs.map +1 -1
- package/dist/esm/connectkit/index.native.js +7 -5
- package/dist/esm/connectkit/index.native.js.map +1 -1
- package/dist/jsx/connectkit/components/Common/Button/index.mjs +1 -0
- package/dist/jsx/connectkit/components/Common/Button/index.mjs.map +1 -1
- package/dist/jsx/connectkit/components/Common/Button/index.native.js +3 -0
- package/dist/jsx/connectkit/components/Common/Button/index.native.js.map +1 -1
- package/dist/jsx/connectkit/components/Common/Modal/index.mjs +3 -0
- package/dist/jsx/connectkit/components/Common/Modal/index.mjs.map +1 -1
- package/dist/jsx/connectkit/components/Common/Modal/index.native.js +3 -0
- package/dist/jsx/connectkit/components/Common/Modal/index.native.js.map +1 -1
- package/dist/jsx/connectkit/components/ConnectKit.mjs +4 -1
- package/dist/jsx/connectkit/components/ConnectKit.mjs.map +1 -1
- package/dist/jsx/connectkit/components/ConnectKit.native.js +6 -3
- package/dist/jsx/connectkit/components/ConnectKit.native.js.map +1 -1
- package/dist/jsx/connectkit/components/ConnectModal/index.mjs +8 -3
- package/dist/jsx/connectkit/components/ConnectModal/index.mjs.map +1 -1
- package/dist/jsx/connectkit/components/ConnectModal/index.native.js +8 -3
- package/dist/jsx/connectkit/components/ConnectModal/index.native.js.map +1 -1
- package/dist/jsx/connectkit/components/Pages/SignInWithGpg/index.mjs +110 -0
- package/dist/jsx/connectkit/components/Pages/SignInWithGpg/index.mjs.map +1 -0
- package/dist/jsx/connectkit/components/Pages/SignInWithGpg/index.native.js +162 -0
- package/dist/jsx/connectkit/components/Pages/SignInWithGpg/index.native.js.map +1 -0
- package/dist/jsx/connectkit/components/Standard/GPG/index.mjs +32 -0
- package/dist/jsx/connectkit/components/Standard/GPG/index.mjs.map +1 -0
- package/dist/jsx/connectkit/components/Standard/GPG/index.native.js +80 -0
- package/dist/jsx/connectkit/components/Standard/GPG/index.native.js.map +1 -0
- package/dist/jsx/connectkit/gpg/GpgContext.mjs +11 -0
- package/dist/jsx/connectkit/gpg/GpgContext.mjs.map +1 -0
- package/dist/jsx/connectkit/gpg/GpgContext.native.js +40 -0
- package/dist/jsx/connectkit/gpg/GpgContext.native.js.map +1 -0
- package/dist/jsx/connectkit/gpg/GpgProvider.mjs +86 -0
- package/dist/jsx/connectkit/gpg/GpgProvider.mjs.map +1 -0
- package/dist/jsx/connectkit/gpg/GpgProvider.native.js +127 -0
- package/dist/jsx/connectkit/gpg/GpgProvider.native.js.map +1 -0
- package/dist/jsx/connectkit/gpg/index.mjs +5 -0
- package/dist/jsx/connectkit/gpg/index.mjs.map +1 -0
- package/dist/jsx/connectkit/gpg/index.native.js +37 -0
- package/dist/jsx/connectkit/gpg/index.native.js.map +1 -0
- package/dist/jsx/connectkit/gpg/useGpg.mjs +45 -0
- package/dist/jsx/connectkit/gpg/useGpg.mjs.map +1 -0
- package/dist/jsx/connectkit/gpg/useGpg.native.js +77 -0
- package/dist/jsx/connectkit/gpg/useGpg.native.js.map +1 -0
- package/dist/jsx/connectkit/hooks/useModal.mjs +7 -3
- package/dist/jsx/connectkit/hooks/useModal.mjs.map +1 -1
- package/dist/jsx/connectkit/hooks/useModal.native.js +8 -2
- package/dist/jsx/connectkit/hooks/useModal.native.js.map +1 -1
- package/dist/jsx/connectkit/index.mjs +7 -5
- package/dist/jsx/connectkit/index.mjs.map +1 -1
- package/dist/jsx/connectkit/index.native.js +7 -0
- package/dist/jsx/connectkit/index.native.js.map +1 -1
- package/package.json +1 -1
- package/src/connectkit/components/Common/Button/index.tsx +3 -0
- package/src/connectkit/components/Common/Modal/index.tsx +3 -0
- package/src/connectkit/components/ConnectKit.tsx +4 -0
- package/src/connectkit/components/ConnectModal/index.tsx +10 -2
- package/src/connectkit/components/Pages/SignInWithGpg/index.tsx +102 -0
- package/src/connectkit/components/Standard/GPG/index.tsx +37 -0
- package/src/connectkit/gpg/GpgContext.tsx +49 -0
- package/src/connectkit/gpg/GpgProvider.tsx +131 -0
- package/src/connectkit/gpg/index.ts +4 -0
- package/src/connectkit/gpg/useGpg.ts +57 -0
- package/src/connectkit/hooks/useModal.ts +10 -1
- package/src/connectkit/index.ts +11 -0
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import React, { CSSProperties, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
import { useContext } from '../../ConnectKit';
|
|
4
|
+
import { PageContent, ModalBody, ModalContent } from '../../Common/Modal/styles';
|
|
5
|
+
import Button from '../../Common/Button';
|
|
6
|
+
import CopyToClipboard from '../../Common/CopyToClipboard';
|
|
7
|
+
import { useGpg } from '../../../gpg';
|
|
8
|
+
|
|
9
|
+
// Sign-In With GPG screen: a connection-free, copy/paste flow rendered inside the SAME
|
|
10
|
+
// ConnectKit modal as the wallet screens. Reads everything from useGpg() (the consumer's
|
|
11
|
+
// GpgProvider supplies the backend-specific getNonce/onSign) — no keycloak knowledge here.
|
|
12
|
+
const textarea: CSSProperties = {
|
|
13
|
+
width: '100%',
|
|
14
|
+
boxSizing: 'border-box',
|
|
15
|
+
fontFamily: 'var(--ck-font-family, monospace)',
|
|
16
|
+
fontSize: 11,
|
|
17
|
+
lineHeight: 1.4,
|
|
18
|
+
padding: 8,
|
|
19
|
+
borderRadius: 'var(--ck-primary-button-border-radius, 12px)',
|
|
20
|
+
border: '1px solid var(--ck-body-divider, #d3d3d3)',
|
|
21
|
+
background: 'var(--ck-body-background-secondary, #f6f7f9)',
|
|
22
|
+
color: 'var(--ck-body-color, inherit)',
|
|
23
|
+
resize: 'vertical',
|
|
24
|
+
};
|
|
25
|
+
const label: CSSProperties = {
|
|
26
|
+
textAlign: 'left',
|
|
27
|
+
fontSize: 14,
|
|
28
|
+
fontWeight: 500,
|
|
29
|
+
margin: '4px 0',
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const SignInWithGpg: React.FC = () => {
|
|
33
|
+
const context = useContext();
|
|
34
|
+
const { message, command, signIn, isLoading, isReady } = useGpg();
|
|
35
|
+
const [pasted, setPasted] = useState('');
|
|
36
|
+
const [error, setError] = useState<string | null>(null);
|
|
37
|
+
const [busy, setBusy] = useState(false);
|
|
38
|
+
|
|
39
|
+
const onSubmit = async () => {
|
|
40
|
+
setError(null);
|
|
41
|
+
setBusy(true);
|
|
42
|
+
try {
|
|
43
|
+
await signIn(pasted);
|
|
44
|
+
// A keycloak browser login navigates away; for API consumers, close the modal.
|
|
45
|
+
setTimeout(() => context.setOpen(false), 1000);
|
|
46
|
+
} catch (e) {
|
|
47
|
+
setError(e instanceof Error ? e.message : 'GPG sign-in failed');
|
|
48
|
+
setBusy(false);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<PageContent style={{ width: 320 }}>
|
|
54
|
+
<ModalContent style={{ paddingBottom: 8, gap: 6 }}>
|
|
55
|
+
<ModalBody style={label}>1. Sign the challenge in your terminal:</ModalBody>
|
|
56
|
+
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
|
57
|
+
<CopyToClipboard variant="button" string={command ?? ''}>
|
|
58
|
+
Copy gpg command
|
|
59
|
+
</CopyToClipboard>
|
|
60
|
+
<CopyToClipboard variant="button" string={message ?? ''}>
|
|
61
|
+
Copy message only
|
|
62
|
+
</CopyToClipboard>
|
|
63
|
+
</div>
|
|
64
|
+
<textarea
|
|
65
|
+
readOnly
|
|
66
|
+
rows={4}
|
|
67
|
+
value={message ?? (isLoading ? 'Loading challenge…' : '')}
|
|
68
|
+
style={textarea}
|
|
69
|
+
/>
|
|
70
|
+
<ModalBody style={label}>2. Paste the full output below:</ModalBody>
|
|
71
|
+
<textarea
|
|
72
|
+
rows={6}
|
|
73
|
+
value={pasted}
|
|
74
|
+
onChange={(e) => setPasted(e.target.value)}
|
|
75
|
+
placeholder="-----BEGIN PGP SIGNATURE----- … and your -----BEGIN PGP PUBLIC KEY BLOCK-----"
|
|
76
|
+
style={textarea}
|
|
77
|
+
/>
|
|
78
|
+
<Button
|
|
79
|
+
onClick={onSubmit}
|
|
80
|
+
disabled={!isReady || !pasted || busy}
|
|
81
|
+
waiting={busy}
|
|
82
|
+
arrow
|
|
83
|
+
>
|
|
84
|
+
Sign in
|
|
85
|
+
</Button>
|
|
86
|
+
{error && (
|
|
87
|
+
<ModalBody
|
|
88
|
+
style={{
|
|
89
|
+
color: 'var(--ck-body-color-danger, #c9190b)',
|
|
90
|
+
fontSize: 13,
|
|
91
|
+
textAlign: 'left',
|
|
92
|
+
}}
|
|
93
|
+
>
|
|
94
|
+
{error}
|
|
95
|
+
</ModalBody>
|
|
96
|
+
)}
|
|
97
|
+
</ModalContent>
|
|
98
|
+
</PageContent>
|
|
99
|
+
);
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export default SignInWithGpg;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import Button from '../../Common/Button';
|
|
3
|
+
import { ResetContainer } from '../../../styles';
|
|
4
|
+
import { useModal } from '../../../hooks/useModal';
|
|
5
|
+
|
|
6
|
+
// Standalone trigger for the GPG flow. GPG is NOT a wallet connector — there is nothing
|
|
7
|
+
// to connect first — so this button simply opens the SAME modal straight to the GPG
|
|
8
|
+
// screen (no connect step), mirroring how the SIWE button opens its screen.
|
|
9
|
+
type ButtonProps = {
|
|
10
|
+
children?: React.ReactNode;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const GPGButton: React.FC<ButtonProps> = ({ children }) => {
|
|
14
|
+
const { openGpg } = useModal();
|
|
15
|
+
return (
|
|
16
|
+
<Button
|
|
17
|
+
key="button"
|
|
18
|
+
style={{ margin: 0 }}
|
|
19
|
+
arrow
|
|
20
|
+
// preventDefault so we never submit a surrounding host <form> (e.g. keycloak's
|
|
21
|
+
// login form) — the underlying styled button can render as type=submit.
|
|
22
|
+
onClick={(e?: { preventDefault?: () => void }) => {
|
|
23
|
+
e?.preventDefault?.();
|
|
24
|
+
openGpg();
|
|
25
|
+
}}
|
|
26
|
+
>
|
|
27
|
+
{children ?? 'Sign in with GPG'}
|
|
28
|
+
</Button>
|
|
29
|
+
);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const GPGButtonComponent: React.FC<ButtonProps> = ({ ...props }) => (
|
|
33
|
+
<ResetContainer>
|
|
34
|
+
<GPGButton {...props} />
|
|
35
|
+
</ResetContainer>
|
|
36
|
+
);
|
|
37
|
+
export default GPGButtonComponent;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { createContext } from 'react';
|
|
2
|
+
import { useQuery } from '@tanstack/react-query';
|
|
3
|
+
|
|
4
|
+
// Sign-In With GPG (SIWG) is a SEPARATE, connection-free signer: unlike a wallet
|
|
5
|
+
// there is nothing to "connect" first — the user signs a challenge in their terminal
|
|
6
|
+
// and pastes the result back. This module deliberately pulls in NO wagmi/viem, so it
|
|
7
|
+
// can later move into a standalone `@multiplatform.one/gpg` package on top of connectkit.
|
|
8
|
+
|
|
9
|
+
export enum GpgStatusState {
|
|
10
|
+
READY = 'ready',
|
|
11
|
+
LOADING = 'loading',
|
|
12
|
+
SUCCESS = 'success',
|
|
13
|
+
ERROR = 'error',
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type GpgConfig = {
|
|
17
|
+
// Required: fetch a fresh nonce from the backend (e.g. keycloak /realms/<realm>/siww/nonce).
|
|
18
|
+
getNonce: () => Promise<string>;
|
|
19
|
+
// Required: submit the signed credential. The screen produces
|
|
20
|
+
// username = base64(armoredSignature + armoredPublicKey)
|
|
21
|
+
// password = base64(message)
|
|
22
|
+
// For a keycloak browser login this writes the login form and submits it (the page
|
|
23
|
+
// then navigates); an API consumer may POST and return a boolean instead.
|
|
24
|
+
onSign: (cred: {
|
|
25
|
+
username: string;
|
|
26
|
+
password: string;
|
|
27
|
+
message: string;
|
|
28
|
+
}) => Promise<boolean | void> | boolean | void;
|
|
29
|
+
|
|
30
|
+
// Optional: override the challenge message (default = the standard SIWG message).
|
|
31
|
+
buildMessage?: (args: { nonce: string }) => string;
|
|
32
|
+
enabled?: boolean;
|
|
33
|
+
nonceRefetchInterval?: number;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export type GpgContextValue = Required<
|
|
37
|
+
Pick<GpgConfig, 'getNonce' | 'onSign' | 'enabled' | 'nonceRefetchInterval'>
|
|
38
|
+
> & {
|
|
39
|
+
buildMessage?: GpgConfig['buildMessage'];
|
|
40
|
+
nonce: ReturnType<typeof useQuery<string | null>>;
|
|
41
|
+
// Derived from the nonce — what the user copies/signs, and the ready-to-run command.
|
|
42
|
+
message: string | null;
|
|
43
|
+
command: string | null;
|
|
44
|
+
status: GpgStatusState;
|
|
45
|
+
signIn: (pastedBlob: string) => Promise<boolean>;
|
|
46
|
+
resetStatus: () => void;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const GpgContext = createContext<GpgContextValue | null>(null);
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { ReactNode, useContext, useMemo, useState } from 'react';
|
|
2
|
+
import { useQuery } from '@tanstack/react-query';
|
|
3
|
+
|
|
4
|
+
import { Context as ConnectKitContext } from '../components/ConnectKit';
|
|
5
|
+
import { GpgContext, GpgConfig, GpgStatusState } from './GpgContext';
|
|
6
|
+
|
|
7
|
+
type Props = GpgConfig & {
|
|
8
|
+
children: ReactNode;
|
|
9
|
+
onSignIn?: () => void;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const GPG_NONCE_QUERY_KEY = 'ckGpgNonce';
|
|
13
|
+
|
|
14
|
+
// UTF-8-safe base64 — the server decodes with new String(decoder.decode(...), UTF_8),
|
|
15
|
+
// so plain btoa would corrupt non-ASCII. Must keep the unescape(encodeURIComponent()).
|
|
16
|
+
const b64 = (s: string) => window.btoa(unescape(encodeURIComponent(s)));
|
|
17
|
+
|
|
18
|
+
// The default SIWG challenge. Only the header line (domain) and the Nonce line are
|
|
19
|
+
// load-bearing per the server's GpgMessage grammar; the rest is a cosmetic envelope.
|
|
20
|
+
function defaultBuildMessage({ nonce }: { nonce: string }): string {
|
|
21
|
+
return (
|
|
22
|
+
`${window.location.host} wants you to sign in with your GPG key.\n\n` +
|
|
23
|
+
`URI: ${window.location.origin}\n` +
|
|
24
|
+
`Version: 1\n` +
|
|
25
|
+
`Nonce: ${nonce}\n` +
|
|
26
|
+
`Issued At: ${new Date().toISOString()}`
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const GpgProvider = ({
|
|
31
|
+
children,
|
|
32
|
+
enabled = true,
|
|
33
|
+
nonceRefetchInterval = 1000 * 60 * 5,
|
|
34
|
+
buildMessage,
|
|
35
|
+
onSignIn,
|
|
36
|
+
...gpgConfig
|
|
37
|
+
}: Props) => {
|
|
38
|
+
const [status, setStatus] = useState<GpgStatusState>(GpgStatusState.READY);
|
|
39
|
+
const resetStatus = () => setStatus(GpgStatusState.READY);
|
|
40
|
+
|
|
41
|
+
// Mirror SIWEProvider's guards: single instance, and mounted OUTSIDE ConnectKitProvider
|
|
42
|
+
// so the modal's GPG screen can read this context.
|
|
43
|
+
if (useContext(GpgContext)) {
|
|
44
|
+
throw new Error(
|
|
45
|
+
'Multiple, nested usages of GpgProvider detected. Please use only one.'
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
if (useContext(ConnectKitContext)) {
|
|
49
|
+
throw new Error('ConnectKitProvider must be mounted inside GpgProvider.');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const nonce = useQuery({
|
|
53
|
+
queryKey: [GPG_NONCE_QUERY_KEY],
|
|
54
|
+
queryFn: () => gpgConfig.getNonce(),
|
|
55
|
+
refetchInterval: nonceRefetchInterval,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Compute the challenge ONCE per nonce (it embeds an Issued At timestamp); recomputing
|
|
59
|
+
// on every render would desync the displayed/copied message from the one base64'd into
|
|
60
|
+
// `password`, breaking the signature (which is verified over the exact message bytes).
|
|
61
|
+
const message = useMemo(
|
|
62
|
+
() =>
|
|
63
|
+
nonce.data
|
|
64
|
+
? (buildMessage ?? defaultBuildMessage)({ nonce: nonce.data })
|
|
65
|
+
: null,
|
|
66
|
+
[nonce.data, buildMessage]
|
|
67
|
+
);
|
|
68
|
+
// The ready-to-run terminal command: detached armored signature + the exported public
|
|
69
|
+
// key, concatenated — exactly the blob the user pastes back. printf %s (no newline) so
|
|
70
|
+
// the signed bytes are byte-identical to `message`.
|
|
71
|
+
const command = useMemo(
|
|
72
|
+
() =>
|
|
73
|
+
message
|
|
74
|
+
? `{ printf %s '${message}' | gpg --detach-sign --armor; gpg --armor --export; }`
|
|
75
|
+
: null,
|
|
76
|
+
[message]
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
const signIn = async (pastedBlob: string): Promise<boolean> => {
|
|
80
|
+
const blob = pastedBlob.trim();
|
|
81
|
+
if (
|
|
82
|
+
!blob.includes('-----BEGIN PGP SIGNATURE-----') ||
|
|
83
|
+
!blob.includes('-----BEGIN PGP PUBLIC KEY BLOCK-----')
|
|
84
|
+
) {
|
|
85
|
+
setStatus(GpgStatusState.ERROR);
|
|
86
|
+
throw new Error(
|
|
87
|
+
'Paste the FULL output — it must contain both a PGP SIGNATURE and a PUBLIC KEY block.'
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
if (!message) {
|
|
91
|
+
setStatus(GpgStatusState.ERROR);
|
|
92
|
+
throw new Error('Could not fetch nonce');
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
setStatus(GpgStatusState.LOADING);
|
|
96
|
+
// username = base64(signature + public key), password = base64(message).
|
|
97
|
+
const result = await gpgConfig.onSign({
|
|
98
|
+
username: b64(blob),
|
|
99
|
+
password: b64(message),
|
|
100
|
+
message,
|
|
101
|
+
});
|
|
102
|
+
// A keycloak browser login navigates away here; an API consumer returns a boolean.
|
|
103
|
+
if (result === false) throw new Error('GPG sign-in was rejected');
|
|
104
|
+
setStatus(GpgStatusState.SUCCESS);
|
|
105
|
+
onSignIn?.();
|
|
106
|
+
return true;
|
|
107
|
+
} catch (error) {
|
|
108
|
+
setStatus(GpgStatusState.ERROR);
|
|
109
|
+
throw error;
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<GpgContext.Provider
|
|
115
|
+
value={{
|
|
116
|
+
enabled,
|
|
117
|
+
nonceRefetchInterval,
|
|
118
|
+
buildMessage,
|
|
119
|
+
...gpgConfig,
|
|
120
|
+
nonce,
|
|
121
|
+
message,
|
|
122
|
+
command,
|
|
123
|
+
status,
|
|
124
|
+
signIn,
|
|
125
|
+
resetStatus,
|
|
126
|
+
}}
|
|
127
|
+
>
|
|
128
|
+
{children}
|
|
129
|
+
</GpgContext.Provider>
|
|
130
|
+
);
|
|
131
|
+
};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { useContext } from 'react';
|
|
2
|
+
import { GpgContext, GpgStatusState } from './GpgContext';
|
|
3
|
+
|
|
4
|
+
type HookProps = {
|
|
5
|
+
// What the GPG screen renders:
|
|
6
|
+
message: string | null; // the challenge to copy/sign
|
|
7
|
+
command: string | null; // the ready-to-run terminal command
|
|
8
|
+
// State:
|
|
9
|
+
status: GpgStatusState;
|
|
10
|
+
error?: Error | any;
|
|
11
|
+
isLoading: boolean;
|
|
12
|
+
isSuccess: boolean;
|
|
13
|
+
isError: boolean;
|
|
14
|
+
isReady: boolean;
|
|
15
|
+
// Actions:
|
|
16
|
+
signIn: (pastedBlob: string) => Promise<boolean>;
|
|
17
|
+
reset: () => void;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// Consumer-facing hook. Degrades gracefully (returns a disabled shape) when no
|
|
21
|
+
// GpgProvider is mounted, so non-GPG apps don't crash — same contract as useSIWE.
|
|
22
|
+
export const useGpg = (): HookProps | any => {
|
|
23
|
+
const gpg = useContext(GpgContext);
|
|
24
|
+
if (!gpg) {
|
|
25
|
+
return {
|
|
26
|
+
message: null,
|
|
27
|
+
command: null,
|
|
28
|
+
status: GpgStatusState.ERROR,
|
|
29
|
+
error: new Error('useGpg hook must be inside a GpgProvider.'),
|
|
30
|
+
isLoading: false,
|
|
31
|
+
isSuccess: false,
|
|
32
|
+
isError: true,
|
|
33
|
+
isReady: false,
|
|
34
|
+
signIn: () => Promise.reject(),
|
|
35
|
+
reset: () => {},
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const { nonce, message, command, status, signIn, resetStatus } = gpg;
|
|
40
|
+
const isLoading = status === GpgStatusState.LOADING || nonce.isLoading;
|
|
41
|
+
const isSuccess = status === GpgStatusState.SUCCESS;
|
|
42
|
+
const isError = status === GpgStatusState.ERROR;
|
|
43
|
+
const isReady = !!message && !isLoading;
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
message,
|
|
47
|
+
command,
|
|
48
|
+
status,
|
|
49
|
+
error: nonce.error,
|
|
50
|
+
isLoading,
|
|
51
|
+
isSuccess,
|
|
52
|
+
isError,
|
|
53
|
+
isReady,
|
|
54
|
+
signIn,
|
|
55
|
+
reset: () => resetStatus(),
|
|
56
|
+
};
|
|
57
|
+
};
|
|
@@ -18,8 +18,15 @@ const safeRoutes: {
|
|
|
18
18
|
routes.ONBOARDING,
|
|
19
19
|
routes.MOBILECONNECTORS,
|
|
20
20
|
routes.ONBOARDING,
|
|
21
|
+
// GPG has no connect step — it's valid in both connected and disconnected states.
|
|
22
|
+
routes.SIGNINWITHGPG,
|
|
23
|
+
],
|
|
24
|
+
connected: [
|
|
25
|
+
routes.PROFILE,
|
|
26
|
+
routes.SWITCHNETWORKS,
|
|
27
|
+
routes.SIGNINWITHETHEREUM,
|
|
28
|
+
routes.SIGNINWITHGPG,
|
|
21
29
|
],
|
|
22
|
-
connected: [routes.PROFILE, routes.SWITCHNETWORKS, routes.SIGNINWITHETHEREUM],
|
|
23
30
|
};
|
|
24
31
|
const allRoutes: ModalRoutes[] = [
|
|
25
32
|
...safeRoutes.connected,
|
|
@@ -97,5 +104,7 @@ export const useModal = ({ onConnect, onDisconnect }: UseModalProps = {}) => {
|
|
|
97
104
|
gotoAndOpen(routes.SIGNINWITHETHEREUM);
|
|
98
105
|
if (triggerSIWE) signIn();
|
|
99
106
|
},
|
|
107
|
+
// Open the SAME modal straight to the GPG screen — no wallet connect step.
|
|
108
|
+
openGpg: () => gotoAndOpen(routes.SIGNINWITHGPG),
|
|
100
109
|
};
|
|
101
110
|
};
|
package/src/connectkit/index.ts
CHANGED
|
@@ -14,9 +14,20 @@ export {
|
|
|
14
14
|
} from './siwe';
|
|
15
15
|
export type { SIWESession, SIWEConfig } from './siwe';
|
|
16
16
|
|
|
17
|
+
// Sign-In With GPG (SIWG) — a separate, connection-free signer that opens the SAME
|
|
18
|
+
// modal to a GPG screen. Pulls in NO wagmi/viem; destined for a standalone gpg package.
|
|
19
|
+
export {
|
|
20
|
+
GpgProvider,
|
|
21
|
+
useGpg,
|
|
22
|
+
GpgStatusState,
|
|
23
|
+
GPG_NONCE_QUERY_KEY,
|
|
24
|
+
} from './gpg';
|
|
25
|
+
export type { GpgConfig } from './gpg';
|
|
26
|
+
|
|
17
27
|
export { ConnectKitProvider, Context } from './components/ConnectKit';
|
|
18
28
|
export { ConnectKitButton } from './components/ConnectButton';
|
|
19
29
|
export { default as SIWEButton } from './components/Standard/SIWE';
|
|
30
|
+
export { default as SignInWithGpgButton } from './components/Standard/GPG';
|
|
20
31
|
|
|
21
32
|
//export { default as NetworkButton } from './components/NetworkButton';
|
|
22
33
|
//export { default as BalanceButton, Balance } from './components/BalanceButton';
|