@multiplatform.one/web3 5.0.27 → 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.
Files changed (145) hide show
  1. package/dist/cjs/connectkit/components/Common/Button/index.cjs +1 -0
  2. package/dist/cjs/connectkit/components/Common/Button/index.native.js +3 -0
  3. package/dist/cjs/connectkit/components/Common/Button/index.native.js.map +1 -1
  4. package/dist/cjs/connectkit/components/Common/Modal/index.cjs +3 -0
  5. package/dist/cjs/connectkit/components/Common/Modal/index.native.js +3 -0
  6. package/dist/cjs/connectkit/components/Common/Modal/index.native.js.map +1 -1
  7. package/dist/cjs/connectkit/components/ConnectKit.cjs +4 -1
  8. package/dist/cjs/connectkit/components/ConnectKit.native.js +6 -3
  9. package/dist/cjs/connectkit/components/ConnectKit.native.js.map +1 -1
  10. package/dist/cjs/connectkit/components/ConnectModal/index.cjs +8 -3
  11. package/dist/cjs/connectkit/components/ConnectModal/index.native.js +8 -3
  12. package/dist/cjs/connectkit/components/ConnectModal/index.native.js.map +1 -1
  13. package/dist/cjs/connectkit/components/Pages/SignInWithGpg/index.cjs +146 -0
  14. package/dist/cjs/connectkit/components/Pages/SignInWithGpg/index.native.js +162 -0
  15. package/dist/cjs/connectkit/components/Pages/SignInWithGpg/index.native.js.map +1 -0
  16. package/dist/cjs/connectkit/components/Standard/GPG/index.cjs +69 -0
  17. package/dist/cjs/connectkit/components/Standard/GPG/index.native.js +80 -0
  18. package/dist/cjs/connectkit/components/Standard/GPG/index.native.js.map +1 -0
  19. package/dist/cjs/connectkit/gpg/GpgContext.cjs +37 -0
  20. package/dist/cjs/connectkit/gpg/GpgContext.native.js +40 -0
  21. package/dist/cjs/connectkit/gpg/GpgContext.native.js.map +1 -0
  22. package/dist/cjs/connectkit/gpg/GpgProvider.cjs +112 -0
  23. package/dist/cjs/connectkit/gpg/GpgProvider.native.js +127 -0
  24. package/dist/cjs/connectkit/gpg/GpgProvider.native.js.map +1 -0
  25. package/dist/cjs/connectkit/gpg/index.cjs +34 -0
  26. package/dist/cjs/connectkit/gpg/index.native.js +37 -0
  27. package/dist/cjs/connectkit/gpg/index.native.js.map +1 -0
  28. package/dist/cjs/connectkit/gpg/useGpg.cjs +70 -0
  29. package/dist/cjs/connectkit/gpg/useGpg.native.js +77 -0
  30. package/dist/cjs/connectkit/gpg/useGpg.native.js.map +1 -0
  31. package/dist/cjs/connectkit/hooks/useModal.cjs +7 -3
  32. package/dist/cjs/connectkit/hooks/useModal.native.js +8 -2
  33. package/dist/cjs/connectkit/hooks/useModal.native.js.map +1 -1
  34. package/dist/cjs/connectkit/index.cjs +7 -0
  35. package/dist/cjs/connectkit/index.native.js +7 -0
  36. package/dist/cjs/connectkit/index.native.js.map +1 -1
  37. package/dist/esm/connectkit/components/Common/Button/index.mjs +1 -0
  38. package/dist/esm/connectkit/components/Common/Button/index.mjs.map +1 -1
  39. package/dist/esm/connectkit/components/Common/Button/index.native.js +3 -0
  40. package/dist/esm/connectkit/components/Common/Button/index.native.js.map +1 -1
  41. package/dist/esm/connectkit/components/Common/Modal/index.mjs +3 -0
  42. package/dist/esm/connectkit/components/Common/Modal/index.mjs.map +1 -1
  43. package/dist/esm/connectkit/components/Common/Modal/index.native.js +3 -0
  44. package/dist/esm/connectkit/components/Common/Modal/index.native.js.map +1 -1
  45. package/dist/esm/connectkit/components/ConnectKit.mjs +4 -1
  46. package/dist/esm/connectkit/components/ConnectKit.mjs.map +1 -1
  47. package/dist/esm/connectkit/components/ConnectKit.native.js +6 -3
  48. package/dist/esm/connectkit/components/ConnectKit.native.js.map +1 -1
  49. package/dist/esm/connectkit/components/ConnectModal/index.mjs +8 -3
  50. package/dist/esm/connectkit/components/ConnectModal/index.mjs.map +1 -1
  51. package/dist/esm/connectkit/components/ConnectModal/index.native.js +8 -3
  52. package/dist/esm/connectkit/components/ConnectModal/index.native.js.map +1 -1
  53. package/dist/esm/connectkit/components/Pages/SignInWithGpg/index.mjs +110 -0
  54. package/dist/esm/connectkit/components/Pages/SignInWithGpg/index.mjs.map +1 -0
  55. package/dist/esm/connectkit/components/Pages/SignInWithGpg/index.native.js +123 -0
  56. package/dist/esm/connectkit/components/Pages/SignInWithGpg/index.native.js.map +1 -0
  57. package/dist/esm/connectkit/components/Standard/GPG/index.mjs +32 -0
  58. package/dist/esm/connectkit/components/Standard/GPG/index.mjs.map +1 -0
  59. package/dist/esm/connectkit/components/Standard/GPG/index.native.js +40 -0
  60. package/dist/esm/connectkit/components/Standard/GPG/index.native.js.map +1 -0
  61. package/dist/esm/connectkit/gpg/GpgContext.mjs +11 -0
  62. package/dist/esm/connectkit/gpg/GpgContext.mjs.map +1 -0
  63. package/dist/esm/connectkit/gpg/GpgContext.native.js +11 -0
  64. package/dist/esm/connectkit/gpg/GpgContext.native.js.map +1 -0
  65. package/dist/esm/connectkit/gpg/GpgProvider.mjs +86 -0
  66. package/dist/esm/connectkit/gpg/GpgProvider.mjs.map +1 -0
  67. package/dist/esm/connectkit/gpg/GpgProvider.native.js +98 -0
  68. package/dist/esm/connectkit/gpg/GpgProvider.native.js.map +1 -0
  69. package/dist/esm/connectkit/gpg/index.mjs +5 -0
  70. package/dist/esm/connectkit/gpg/index.mjs.map +1 -0
  71. package/dist/esm/connectkit/gpg/index.native.js +5 -0
  72. package/dist/esm/connectkit/gpg/index.native.js.map +1 -0
  73. package/dist/esm/connectkit/gpg/useGpg.mjs +45 -0
  74. package/dist/esm/connectkit/gpg/useGpg.mjs.map +1 -0
  75. package/dist/esm/connectkit/gpg/useGpg.native.js +49 -0
  76. package/dist/esm/connectkit/gpg/useGpg.native.js.map +1 -0
  77. package/dist/esm/connectkit/hooks/useModal.mjs +7 -3
  78. package/dist/esm/connectkit/hooks/useModal.mjs.map +1 -1
  79. package/dist/esm/connectkit/hooks/useModal.native.js +8 -2
  80. package/dist/esm/connectkit/hooks/useModal.native.js.map +1 -1
  81. package/dist/esm/connectkit/index.mjs +7 -5
  82. package/dist/esm/connectkit/index.mjs.map +1 -1
  83. package/dist/esm/connectkit/index.native.js +7 -5
  84. package/dist/esm/connectkit/index.native.js.map +1 -1
  85. package/dist/jsx/connectkit/components/Common/Button/index.mjs +1 -0
  86. package/dist/jsx/connectkit/components/Common/Button/index.mjs.map +1 -1
  87. package/dist/jsx/connectkit/components/Common/Button/index.native.js +3 -0
  88. package/dist/jsx/connectkit/components/Common/Button/index.native.js.map +1 -1
  89. package/dist/jsx/connectkit/components/Common/Modal/index.mjs +3 -0
  90. package/dist/jsx/connectkit/components/Common/Modal/index.mjs.map +1 -1
  91. package/dist/jsx/connectkit/components/Common/Modal/index.native.js +3 -0
  92. package/dist/jsx/connectkit/components/Common/Modal/index.native.js.map +1 -1
  93. package/dist/jsx/connectkit/components/ConnectKit.mjs +4 -1
  94. package/dist/jsx/connectkit/components/ConnectKit.mjs.map +1 -1
  95. package/dist/jsx/connectkit/components/ConnectKit.native.js +6 -3
  96. package/dist/jsx/connectkit/components/ConnectKit.native.js.map +1 -1
  97. package/dist/jsx/connectkit/components/ConnectModal/index.mjs +8 -3
  98. package/dist/jsx/connectkit/components/ConnectModal/index.mjs.map +1 -1
  99. package/dist/jsx/connectkit/components/ConnectModal/index.native.js +8 -3
  100. package/dist/jsx/connectkit/components/ConnectModal/index.native.js.map +1 -1
  101. package/dist/jsx/connectkit/components/Pages/SignInWithGpg/index.mjs +110 -0
  102. package/dist/jsx/connectkit/components/Pages/SignInWithGpg/index.mjs.map +1 -0
  103. package/dist/jsx/connectkit/components/Pages/SignInWithGpg/index.native.js +162 -0
  104. package/dist/jsx/connectkit/components/Pages/SignInWithGpg/index.native.js.map +1 -0
  105. package/dist/jsx/connectkit/components/Standard/GPG/index.mjs +32 -0
  106. package/dist/jsx/connectkit/components/Standard/GPG/index.mjs.map +1 -0
  107. package/dist/jsx/connectkit/components/Standard/GPG/index.native.js +80 -0
  108. package/dist/jsx/connectkit/components/Standard/GPG/index.native.js.map +1 -0
  109. package/dist/jsx/connectkit/gpg/GpgContext.mjs +11 -0
  110. package/dist/jsx/connectkit/gpg/GpgContext.mjs.map +1 -0
  111. package/dist/jsx/connectkit/gpg/GpgContext.native.js +40 -0
  112. package/dist/jsx/connectkit/gpg/GpgContext.native.js.map +1 -0
  113. package/dist/jsx/connectkit/gpg/GpgProvider.mjs +86 -0
  114. package/dist/jsx/connectkit/gpg/GpgProvider.mjs.map +1 -0
  115. package/dist/jsx/connectkit/gpg/GpgProvider.native.js +127 -0
  116. package/dist/jsx/connectkit/gpg/GpgProvider.native.js.map +1 -0
  117. package/dist/jsx/connectkit/gpg/index.mjs +5 -0
  118. package/dist/jsx/connectkit/gpg/index.mjs.map +1 -0
  119. package/dist/jsx/connectkit/gpg/index.native.js +37 -0
  120. package/dist/jsx/connectkit/gpg/index.native.js.map +1 -0
  121. package/dist/jsx/connectkit/gpg/useGpg.mjs +45 -0
  122. package/dist/jsx/connectkit/gpg/useGpg.mjs.map +1 -0
  123. package/dist/jsx/connectkit/gpg/useGpg.native.js +77 -0
  124. package/dist/jsx/connectkit/gpg/useGpg.native.js.map +1 -0
  125. package/dist/jsx/connectkit/hooks/useModal.mjs +7 -3
  126. package/dist/jsx/connectkit/hooks/useModal.mjs.map +1 -1
  127. package/dist/jsx/connectkit/hooks/useModal.native.js +8 -2
  128. package/dist/jsx/connectkit/hooks/useModal.native.js.map +1 -1
  129. package/dist/jsx/connectkit/index.mjs +7 -5
  130. package/dist/jsx/connectkit/index.mjs.map +1 -1
  131. package/dist/jsx/connectkit/index.native.js +7 -0
  132. package/dist/jsx/connectkit/index.native.js.map +1 -1
  133. package/package.json +2 -1
  134. package/src/connectkit/components/Common/Button/index.tsx +3 -0
  135. package/src/connectkit/components/Common/Modal/index.tsx +3 -0
  136. package/src/connectkit/components/ConnectKit.tsx +4 -0
  137. package/src/connectkit/components/ConnectModal/index.tsx +10 -2
  138. package/src/connectkit/components/Pages/SignInWithGpg/index.tsx +102 -0
  139. package/src/connectkit/components/Standard/GPG/index.tsx +37 -0
  140. package/src/connectkit/gpg/GpgContext.tsx +49 -0
  141. package/src/connectkit/gpg/GpgProvider.tsx +131 -0
  142. package/src/connectkit/gpg/index.ts +4 -0
  143. package/src/connectkit/gpg/useGpg.ts +57 -0
  144. package/src/connectkit/hooks/useModal.ts +10 -1
  145. package/src/connectkit/index.ts +11 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@multiplatform.one/web3",
3
- "version": "5.0.27",
3
+ "version": "5.1.0",
4
4
  "description": "Web3 wallet integration for multiplatform.one ecosystem with ConnectKit and wagmi",
5
5
  "keywords": [
6
6
  "connectkit",
@@ -73,6 +73,7 @@
73
73
  "@solana/wallet-adapter-react": "^0.15.39",
74
74
  "@solana/wallet-adapter-wallets": "^0.19.38",
75
75
  "@solana/web3.js": "^1.98.4",
76
+ "@wagmi/connectors": "^8.0.15",
76
77
  "bs58": "^6.0.0",
77
78
  "buffer": "^6.0.3",
78
79
  "detect-browser": "^5.3.0",
@@ -47,6 +47,9 @@ const Button: React.FC<ButtonProps> = ({
47
47
  return (
48
48
  <ButtonContainer
49
49
  as={href ? 'a' : undefined}
50
+ // Never submit a surrounding <form> (e.g. when the modal/buttons are rendered
51
+ // inside a host login form). Anchors don't take a type.
52
+ type={href ? undefined : 'button'}
50
53
  onClick={(event: any) => {
51
54
  if (!disabled && onClick) onClick(event);
52
55
  }}
@@ -352,6 +352,9 @@ const Modal: React.FC<ModalProps> = ({
352
352
  return isSignedIn
353
353
  ? locales.signInWithEthereumScreen_signedIn_heading
354
354
  : locales.signInWithEthereumScreen_signedOut_heading;
355
+ // Hardcoded (vs a locale key) to avoid touching every locale file for Phase 1.
356
+ case routes.SIGNINWITHGPG:
357
+ return 'Sign in with GPG';
355
358
  default:
356
359
  return '';
357
360
  }
@@ -8,6 +8,7 @@ import ConnectKitModal from "../components/ConnectModal";
8
8
  import { ThemeProvider } from "styled-components";
9
9
  import { useThemeFont } from "../hooks/useGoogleFont";
10
10
  import { SIWEContext } from "./../siwe";
11
+ import { GpgContext } from "./../gpg";
11
12
  import { useChains } from "../hooks/useChains";
12
13
  import { useConnectCallback, useConnectCallbackProps } from "../hooks/useConnectCallback";
13
14
  import { useConnector } from "../hooks/useConnectors";
@@ -26,6 +27,7 @@ export const routes = {
26
27
  PROFILE: "profile",
27
28
  SWITCHNETWORKS: "switchNetworks",
28
29
  SIGNINWITHETHEREUM: "signInWithEthereum",
30
+ SIGNINWITHGPG: "signInWithGpg",
29
31
  };
30
32
 
31
33
  type Connector = {
@@ -51,6 +53,7 @@ type ContextValue = {
51
53
  errorMessage: Error;
52
54
  options?: ConnectKitOptions;
53
55
  signInWithEthereum: boolean;
56
+ signInWithGpg: boolean;
54
57
  debugMode?: boolean;
55
58
  log: (...props: any) => void;
56
59
  displayError: (message: string | React.ReactNode | null, code?: any) => void;
@@ -224,6 +227,7 @@ export const ConnectKitProvider = ({
224
227
  connector,
225
228
  setConnector,
226
229
  signInWithEthereum: React.useContext(SIWEContext)?.enabled ?? false,
230
+ signInWithGpg: React.useContext(GpgContext)?.enabled ?? false,
227
231
  onConnect,
228
232
  // Other configuration
229
233
  options: opts,
@@ -13,6 +13,7 @@ import DownloadApp from '../Pages/DownloadApp';
13
13
  import Profile from '../Pages/Profile';
14
14
  import SwitchNetworks from '../Pages/SwitchNetworks';
15
15
  import SignInWithEthereum from '../Pages/SignInWithEthereum';
16
+ import SignInWithGpg from '../Pages/SignInWithGpg';
16
17
 
17
18
  import { getAppIcon, getAppName } from '../../defaultConfig';
18
19
  import { ConnectKitThemeProvider } from '../ConnectKitThemeProvider/ConnectKitThemeProvider';
@@ -45,9 +46,15 @@ const ConnectModal: React.FC<{
45
46
  const showBackButton =
46
47
  closeable &&
47
48
  context.route !== routes.CONNECTORS &&
48
- context.route !== routes.PROFILE;
49
+ context.route !== routes.PROFILE &&
50
+ // GPG is a standalone, connection-free screen reached via its own trigger, not a
51
+ // sub-screen of the wallet list — so it gets a plain close, no back-to-wallets.
52
+ context.route !== routes.SIGNINWITHGPG;
49
53
 
50
- const showInfoButton = closeable && context.route !== routes.PROFILE;
54
+ const showInfoButton =
55
+ closeable &&
56
+ context.route !== routes.PROFILE &&
57
+ context.route !== routes.SIGNINWITHGPG;
51
58
 
52
59
  const onBack = () => {
53
60
  if (context.route === routes.SIGNINWITHETHEREUM) {
@@ -71,6 +78,7 @@ const ConnectModal: React.FC<{
71
78
  profile: <Profile />,
72
79
  switchNetworks: <SwitchNetworks />,
73
80
  signInWithEthereum: <SignInWithEthereum />,
81
+ signInWithGpg: <SignInWithGpg />,
74
82
  };
75
83
 
76
84
  function hide() {
@@ -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,4 @@
1
+ export { useGpg } from './useGpg';
2
+ export { GpgProvider, GPG_NONCE_QUERY_KEY } from './GpgProvider';
3
+ export { GpgContext, GpgStatusState } from './GpgContext';
4
+ export type { GpgConfig, GpgContextValue } from './GpgContext';
@@ -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
  };
@@ -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';