shogun-button-react 6.4.0 β†’ 6.4.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/README.md CHANGED
@@ -9,7 +9,9 @@ A comprehensive React component library for seamless integration of Shogun authe
9
9
  - πŸš€ **Easy Integration** - Simple setup with minimal configuration
10
10
  - 🎨 **Customizable UI** - Modern, responsive design with dark mode support
11
11
  - πŸ”’ **Multi-Authentication** - Support for Password, MetaMask, WebAuthn, Nostr, and ZK-Proof
12
+ - πŸ›‘οΈ **WebAuthn Recovery** - Restore hardware credentials on new devices with saved seed phrases
12
13
  - πŸ”‘ **Account Management** - Export/import Gun pairs for account backup and recovery
14
+ - πŸ•΅οΈ **ZK-Proof Trapdoor Handoff** - Display and copy the generated trapdoor during signup to keep anonymous identities portable
13
15
  - πŸ“± **Responsive Design** - Works seamlessly across all device sizes
14
16
  - 🌍 **TypeScript Support** - Full type safety and IntelliSense support
15
17
  - πŸ”Œ **Plugin System** - Advanced Gun operations with custom hooks
@@ -170,6 +172,21 @@ const { core, options } = shogunConnector({
170
172
  });
171
173
  ```
172
174
 
175
+ ## πŸ”‘ Recovery Flows
176
+
177
+ ### WebAuthn Multi-Device Restore
178
+
179
+ - Users now see a **Restore with Recovery Code** option when choosing WebAuthn login.
180
+ - Enter the username plus the stored seed phrase to recreate the credential on a new browser.
181
+ - The button calls `webauthnPlugin.signUp(username, { seedPhrase, generateSeedPhrase: false })` behind the scenes, leveraging the core plugin’s `importFromSeed` flow.
182
+ - After a successful restore the seed phrase is shown one more time so the user can double-check or re-copy it before the modal closes.
183
+
184
+ ### ZK-Proof Trapdoor Delivery
185
+
186
+ - Upon successful `zkproof` signup, the modal switches to a confirmation screen that displays the generated trapdoor (also returned as `seedPhrase`).
187
+ - A **Copy Trapdoor** helper copies the phrase to the clipboard, with inline feedback when the copy succeeds.
188
+ - The user must acknowledge with **I Saved My Trapdoor** before returning to the main UI, reducing the risk of losing the anonymous identity.
189
+
173
190
  ## 🎯 API Reference
174
191
 
175
192
  ### ShogunButtonProvider
@@ -23,6 +23,7 @@ type ShogunContextType = {
23
23
  remove: (path: string) => Promise<void>;
24
24
  completePendingSignup: () => void;
25
25
  hasPendingSignup: boolean;
26
+ setHasPendingSignup: (value: boolean) => void;
26
27
  };
27
28
  export declare const useShogun: () => ShogunContextType;
28
29
  type ShogunButtonProviderProps = {
@@ -27,6 +27,7 @@ const defaultShogunContext = {
27
27
  remove: async () => { },
28
28
  completePendingSignup: () => { },
29
29
  hasPendingSignup: false,
30
+ setHasPendingSignup: (_value) => { },
30
31
  };
31
32
  // Create context using React's createContext directly
32
33
  const ShogunContext = createContext(defaultShogunContext);
@@ -38,6 +39,7 @@ export function ShogunButtonProvider({ children, core, options, onLoginSuccess,
38
39
  const [isLoggedIn, setIsLoggedIn] = useState(false);
39
40
  const [userPub, setUserPub] = useState(null);
40
41
  const [username, setUsername] = useState(null);
42
+ const [hasPendingSignup, setHasPendingSignup] = useState(false);
41
43
  // Effetto per gestire l'inizializzazione e pulizia
42
44
  useEffect(() => {
43
45
  var _a, _b;
@@ -220,7 +222,7 @@ export function ShogunButtonProvider({ children, core, options, onLoginSuccess,
220
222
  };
221
223
  // Unified signup
222
224
  const signUp = async (method, ...args) => {
223
- var _a, _b;
225
+ var _a, _b, _c;
224
226
  try {
225
227
  if (!core) {
226
228
  throw new Error("SDK not initialized");
@@ -252,18 +254,39 @@ export function ShogunButtonProvider({ children, core, options, onLoginSuccess,
252
254
  throw error;
253
255
  }
254
256
  break;
255
- case "webauthn":
256
- username = args[0];
257
+ case "webauthn": {
258
+ username = typeof args[0] === "string" ? args[0].trim() : "";
259
+ const webauthnOptions = args.length > 1 && typeof args[1] === "object" && args[1] !== null
260
+ ? args[1]
261
+ : {};
262
+ if (!username) {
263
+ throw new Error("Username is required for WebAuthn registration");
264
+ }
257
265
  if (isShogunCore(core)) {
258
266
  const webauthn = core.getPlugin("webauthn");
259
267
  if (!webauthn)
260
268
  throw new Error("WebAuthn plugin not available");
261
- result = await webauthn.signUp(username, { generateSeedPhrase: true });
269
+ const pluginOptions = {};
270
+ if (webauthnOptions.seedPhrase) {
271
+ pluginOptions.seedPhrase = webauthnOptions.seedPhrase.trim();
272
+ pluginOptions.generateSeedPhrase =
273
+ (_a = webauthnOptions.generateSeedPhrase) !== null && _a !== void 0 ? _a : false;
274
+ }
275
+ else if (typeof webauthnOptions.generateSeedPhrase === "boolean") {
276
+ pluginOptions.generateSeedPhrase =
277
+ webauthnOptions.generateSeedPhrase;
278
+ }
279
+ if (pluginOptions.generateSeedPhrase === undefined &&
280
+ !pluginOptions.seedPhrase) {
281
+ pluginOptions.generateSeedPhrase = true;
282
+ }
283
+ result = await webauthn.signUp(username, pluginOptions);
262
284
  }
263
285
  else {
264
286
  throw new Error("WebAuthn requires ShogunCore");
265
287
  }
266
288
  break;
289
+ }
267
290
  case "web3":
268
291
  if (isShogunCore(core)) {
269
292
  const web3 = core.getPlugin("web3");
@@ -320,7 +343,7 @@ export function ShogunButtonProvider({ children, core, options, onLoginSuccess,
320
343
  if (result.success) {
321
344
  let userPub = result.userPub || "";
322
345
  if (!userPub && isShogunCore(core)) {
323
- userPub = ((_b = (_a = core.gun.user()) === null || _a === void 0 ? void 0 : _a.is) === null || _b === void 0 ? void 0 : _b.pub) || "";
346
+ userPub = ((_c = (_b = core.gun.user()) === null || _b === void 0 ? void 0 : _b.is) === null || _c === void 0 ? void 0 : _c.pub) || "";
324
347
  }
325
348
  const displayName = result.alias || username || userPub.slice(0, 8) + "...";
326
349
  setIsLoggedIn(true);
@@ -332,6 +355,8 @@ export function ShogunButtonProvider({ children, core, options, onLoginSuccess,
332
355
  seedPhrase: result.seedPhrase,
333
356
  authMethod: authMethod,
334
357
  };
358
+ const pendingBackup = Boolean(result.seedPhrase || result.trapdoor);
359
+ setHasPendingSignup(pendingBackup);
335
360
  onSignupSuccess === null || onSignupSuccess === void 0 ? void 0 : onSignupSuccess(signupPayload);
336
361
  }
337
362
  else {
@@ -465,8 +490,9 @@ export function ShogunButtonProvider({ children, core, options, onLoginSuccess,
465
490
  const gunPlugin = null;
466
491
  // Plugin hooks removed - GunAdvancedPlugin no longer available
467
492
  const pluginHooks = {};
468
- const completePendingSignup = React.useCallback(() => { }, []);
469
- const hasPendingSignup = false;
493
+ const completePendingSignup = React.useCallback(() => {
494
+ setHasPendingSignup(false);
495
+ }, [setHasPendingSignup]);
470
496
  // Create a properly typed context value
471
497
  const contextValue = React.useMemo(() => ({
472
498
  core,
@@ -486,6 +512,7 @@ export function ShogunButtonProvider({ children, core, options, onLoginSuccess,
486
512
  gunPlugin,
487
513
  completePendingSignup,
488
514
  hasPendingSignup,
515
+ setHasPendingSignup,
489
516
  put: async (path, data) => {
490
517
  if (isShogunCore(core)) {
491
518
  if (!core.gun)
@@ -545,6 +572,8 @@ export function ShogunButtonProvider({ children, core, options, onLoginSuccess,
545
572
  gunPlugin,
546
573
  pluginHooks,
547
574
  completePendingSignup,
575
+ hasPendingSignup,
576
+ setHasPendingSignup,
548
577
  ]);
549
578
  // Provide the context value to children
550
579
  return (React.createElement(ShogunContext.Provider, { value: contextValue }, children));
@@ -594,7 +623,7 @@ const ExportIcon = () => (React.createElement("svg", { xmlns: "http://www.w3.org
594
623
  // Component for Shogun login button
595
624
  export const ShogunButton = (() => {
596
625
  const Button = () => {
597
- const { isLoggedIn, username, logout, login, signUp, core, options, exportGunPair, importGunPair, hasPlugin, } = useShogun();
626
+ const { isLoggedIn, username, logout, login, signUp, core, options, exportGunPair, importGunPair, hasPlugin, hasPendingSignup, setHasPendingSignup, } = useShogun();
598
627
  // Form states
599
628
  const [modalIsOpen, setModalIsOpen] = useState(false);
600
629
  const [formUsername, setFormUsername] = useState("");
@@ -616,7 +645,10 @@ export const ShogunButton = (() => {
616
645
  const [showCopySuccess, setShowCopySuccess] = useState(false);
617
646
  const [showImportSuccess, setShowImportSuccess] = useState(false);
618
647
  const [zkTrapdoor, setZkTrapdoor] = useState("");
648
+ const [zkSignupTrapdoor, setZkSignupTrapdoor] = useState("");
649
+ const [showZkTrapdoorCopySuccess, setShowZkTrapdoorCopySuccess] = useState(false);
619
650
  const [webauthnSeedPhrase, setWebauthnSeedPhrase] = useState("");
651
+ const [webauthnRecoverySeed, setWebauthnRecoverySeed] = useState("");
620
652
  const dropdownRef = useRef(null);
621
653
  // Handle click outside to close dropdown
622
654
  useEffect(() => {
@@ -633,6 +665,20 @@ export const ShogunButton = (() => {
633
665
  };
634
666
  }
635
667
  }, [dropdownOpen]);
668
+ useEffect(() => {
669
+ if (hasPendingSignup) {
670
+ setModalIsOpen(true);
671
+ if (authView !== "webauthn-signup-result" &&
672
+ authView !== "zkproof-signup-result") {
673
+ if (webauthnSeedPhrase) {
674
+ setAuthView("webauthn-signup-result");
675
+ }
676
+ else if (zkSignupTrapdoor) {
677
+ setAuthView("zkproof-signup-result");
678
+ }
679
+ }
680
+ }
681
+ }, [hasPendingSignup, authView, webauthnSeedPhrase, zkSignupTrapdoor]);
636
682
  // If already logged in, show only logout button
637
683
  if (isLoggedIn && username && !modalIsOpen) {
638
684
  return (React.createElement("div", { className: "shogun-logged-in-container" },
@@ -753,6 +799,41 @@ export const ShogunButton = (() => {
753
799
  }
754
800
  setAuthView("webauthn-username");
755
801
  };
802
+ const handleWebauthnImport = async () => {
803
+ setError("");
804
+ setLoading(true);
805
+ try {
806
+ const username = formUsername.trim();
807
+ const recoveryCode = webauthnRecoverySeed.trim();
808
+ if (!username) {
809
+ throw new Error("Please enter your username");
810
+ }
811
+ if (!recoveryCode) {
812
+ throw new Error("Please enter your recovery code");
813
+ }
814
+ if (!isShogunCore(core)) {
815
+ throw new Error("WebAuthn recovery requires ShogunCore");
816
+ }
817
+ const result = await signUp("webauthn", username, {
818
+ seedPhrase: recoveryCode,
819
+ generateSeedPhrase: false,
820
+ });
821
+ if (!result || !result.success) {
822
+ throw new Error((result === null || result === void 0 ? void 0 : result.error) || "Failed to restore account");
823
+ }
824
+ const seedToDisplay = result.seedPhrase || recoveryCode;
825
+ setWebauthnSeedPhrase(seedToDisplay);
826
+ setWebauthnRecoverySeed("");
827
+ setShowCopySuccess(false);
828
+ setAuthView("webauthn-signup-result");
829
+ }
830
+ catch (e) {
831
+ setError(e.message || "Failed to restore WebAuthn account");
832
+ }
833
+ finally {
834
+ setLoading(false);
835
+ }
836
+ };
756
837
  const handleZkProofAuth = () => {
757
838
  if (!hasPlugin("zkproof")) {
758
839
  setError("ZK-Proof plugin not available");
@@ -791,8 +872,17 @@ export const ShogunButton = (() => {
791
872
  if (!result || !result.success) {
792
873
  throw new Error((result === null || result === void 0 ? void 0 : result.error) || "ZK-Proof signup failed");
793
874
  }
794
- setAuthView("options");
795
- setModalIsOpen(false);
875
+ const trapdoorValue = result.seedPhrase || result.trapdoor || "";
876
+ if (trapdoorValue) {
877
+ setZkSignupTrapdoor(trapdoorValue);
878
+ setShowZkTrapdoorCopySuccess(false);
879
+ setAuthView("zkproof-signup-result");
880
+ setHasPendingSignup(true);
881
+ }
882
+ else {
883
+ setAuthView("options");
884
+ setModalIsOpen(false);
885
+ }
796
886
  }
797
887
  catch (e) {
798
888
  setError(e.message || "ZK-Proof signup failed");
@@ -911,7 +1001,10 @@ export const ShogunButton = (() => {
911
1001
  setShowImportSuccess(false);
912
1002
  setRecoveredHint("");
913
1003
  setZkTrapdoor("");
1004
+ setZkSignupTrapdoor("");
1005
+ setShowZkTrapdoorCopySuccess(false);
914
1006
  setWebauthnSeedPhrase("");
1007
+ setWebauthnRecoverySeed("");
915
1008
  };
916
1009
  const openModal = () => {
917
1010
  resetForm();
@@ -921,11 +1014,13 @@ export const ShogunButton = (() => {
921
1014
  const closeModal = () => {
922
1015
  setError("");
923
1016
  setModalIsOpen(false);
1017
+ setHasPendingSignup(false);
924
1018
  };
925
1019
  const finalizeZkProofSignup = () => {
926
1020
  setError("");
927
1021
  resetForm();
928
1022
  setModalIsOpen(false);
1023
+ setHasPendingSignup(false);
929
1024
  };
930
1025
  const toggleMode = () => {
931
1026
  resetForm();
@@ -1037,7 +1132,52 @@ export const ShogunButton = (() => {
1037
1132
  React.createElement("input", { type: "text", id: "username", value: formUsername, onChange: (e) => setFormUsername(e.target.value), disabled: loading, required: true, placeholder: "Enter your username", autoFocus: true })),
1038
1133
  React.createElement("button", { type: "button", className: "shogun-submit-button", onClick: () => handleAuth("webauthn", formUsername), disabled: loading || !formUsername.trim() }, loading ? "Processing..." : `Continue with WebAuthn`),
1039
1134
  React.createElement("div", { className: "shogun-form-footer" },
1040
- React.createElement("button", { type: "button", className: "shogun-back-button", onClick: () => setAuthView("options"), disabled: loading }, "\u2190 Back to Options"))));
1135
+ React.createElement("button", { type: "button", className: "shogun-back-button", onClick: () => setAuthView("options"), disabled: loading }, "\u2190 Back to Options"),
1136
+ formMode === "login" && (React.createElement("button", { type: "button", className: "shogun-toggle-mode", onClick: () => setAuthView("webauthn-recovery"), disabled: loading }, "Restore with Recovery Code")))));
1137
+ const renderWebauthnRecoveryForm = () => (React.createElement("div", { className: "shogun-auth-form" },
1138
+ React.createElement("h3", null, "Restore WebAuthn Account"),
1139
+ React.createElement("div", { style: {
1140
+ backgroundColor: "#fef3c7",
1141
+ padding: "12px",
1142
+ borderRadius: "8px",
1143
+ marginBottom: "16px",
1144
+ border: "1px solid #f59e0b",
1145
+ } },
1146
+ React.createElement("p", { style: {
1147
+ fontSize: "14px",
1148
+ color: "#92400e",
1149
+ margin: "0",
1150
+ fontWeight: "500",
1151
+ } }, "\u26A0\uFE0F Recovery Required"),
1152
+ React.createElement("p", { style: {
1153
+ fontSize: "13px",
1154
+ color: "#a16207",
1155
+ margin: "4px 0 0 0",
1156
+ } }, "Enter the username and recovery code saved during signup to restore access on this device.")),
1157
+ React.createElement("div", { className: "shogun-form-group" },
1158
+ React.createElement("label", { htmlFor: "recoveryUsername" },
1159
+ React.createElement(UserIcon, null),
1160
+ React.createElement("span", null, "Username")),
1161
+ React.createElement("input", { type: "text", id: "recoveryUsername", value: formUsername, onChange: (e) => setFormUsername(e.target.value), disabled: loading, placeholder: "Enter your username", autoFocus: true })),
1162
+ React.createElement("div", { className: "shogun-form-group" },
1163
+ React.createElement("label", { htmlFor: "recoverySeed" },
1164
+ React.createElement(KeyIcon, null),
1165
+ React.createElement("span", null, "Recovery Code")),
1166
+ React.createElement("textarea", { id: "recoverySeed", value: webauthnRecoverySeed, onChange: (e) => setWebauthnRecoverySeed(e.target.value), disabled: loading, placeholder: "Enter your WebAuthn seed phrase...", rows: 4, style: {
1167
+ fontFamily: "monospace",
1168
+ fontSize: "12px",
1169
+ width: "100%",
1170
+ padding: "8px",
1171
+ border: "1px solid #f59e0b",
1172
+ borderRadius: "4px",
1173
+ backgroundColor: "#fffbeb",
1174
+ } })),
1175
+ React.createElement("button", { type: "button", className: "shogun-submit-button", onClick: handleWebauthnImport, disabled: loading }, loading ? "Restoring..." : "Restore Account"),
1176
+ React.createElement("div", { className: "shogun-form-footer" },
1177
+ React.createElement("button", { type: "button", className: "shogun-back-button", onClick: () => {
1178
+ setError("");
1179
+ setAuthView("webauthn-username");
1180
+ }, disabled: loading }, "\u2190 Back to WebAuthn"))));
1041
1181
  const renderRecoveryForm = () => (React.createElement("div", { className: "shogun-auth-form" },
1042
1182
  React.createElement("div", { className: "shogun-form-group" },
1043
1183
  React.createElement("label", { htmlFor: "username" },
@@ -1150,6 +1290,55 @@ export const ShogunButton = (() => {
1150
1290
  React.createElement("button", { type: "button", className: "shogun-submit-button", onClick: handleZkProofLogin, disabled: loading || !zkTrapdoor.trim() }, loading ? "Processing..." : "Login Anonymously"),
1151
1291
  React.createElement("div", { className: "shogun-form-footer" },
1152
1292
  React.createElement("button", { className: "shogun-toggle-mode", onClick: () => setAuthView("options"), disabled: loading }, "Back to Login Options"))));
1293
+ const renderZkProofSignupResult = () => (React.createElement("div", { className: "shogun-auth-form" },
1294
+ React.createElement("h3", null, "ZK-Proof Account Created!"),
1295
+ React.createElement("div", { style: {
1296
+ backgroundColor: "#fef3c7",
1297
+ padding: "12px",
1298
+ borderRadius: "8px",
1299
+ marginBottom: "16px",
1300
+ border: "1px solid #f59e0b",
1301
+ } },
1302
+ React.createElement("p", { style: {
1303
+ fontSize: "14px",
1304
+ color: "#92400e",
1305
+ margin: "0",
1306
+ fontWeight: "500",
1307
+ } }, "\u26A0\uFE0F Important: Save Your Trapdoor"),
1308
+ React.createElement("p", { style: { fontSize: "13px", color: "#a16207", margin: "4px 0 0 0" } }, "This trapdoor lets you restore your anonymous identity on new devices. Store it securely and never share it.")),
1309
+ React.createElement("div", { className: "shogun-form-group" },
1310
+ React.createElement("label", null, "Your Trapdoor (Recovery Phrase):"),
1311
+ React.createElement("textarea", { value: zkSignupTrapdoor, readOnly: true, rows: 4, style: {
1312
+ fontFamily: "monospace",
1313
+ fontSize: "12px",
1314
+ width: "100%",
1315
+ padding: "8px",
1316
+ border: "2px solid #f59e0b",
1317
+ borderRadius: "4px",
1318
+ backgroundColor: "#fffbeb",
1319
+ } }),
1320
+ React.createElement("button", { type: "button", className: "shogun-submit-button", style: { marginTop: "8px" }, onClick: async () => {
1321
+ if (!zkSignupTrapdoor) {
1322
+ return;
1323
+ }
1324
+ try {
1325
+ if (navigator.clipboard) {
1326
+ await navigator.clipboard.writeText(zkSignupTrapdoor);
1327
+ setShowZkTrapdoorCopySuccess(true);
1328
+ setTimeout(() => setShowZkTrapdoorCopySuccess(false), 3000);
1329
+ }
1330
+ }
1331
+ catch (copyError) {
1332
+ console.warn("Failed to copy trapdoor:", copyError);
1333
+ }
1334
+ }, disabled: !zkSignupTrapdoor }, "Copy Trapdoor"),
1335
+ showZkTrapdoorCopySuccess && (React.createElement("p", { style: {
1336
+ color: "#047857",
1337
+ fontSize: "12px",
1338
+ marginTop: "6px",
1339
+ } }, "Trapdoor copied to clipboard!"))),
1340
+ React.createElement("div", { className: "shogun-form-footer" },
1341
+ React.createElement("button", { type: "button", className: "shogun-submit-button", onClick: finalizeZkProofSignup }, "I Saved My Trapdoor"))));
1153
1342
  const renderWebauthnSignupResult = () => (React.createElement("div", { className: "shogun-auth-form" },
1154
1343
  React.createElement("h3", null, "WebAuthn Account Created!"),
1155
1344
  React.createElement("div", { style: {
@@ -1292,9 +1481,13 @@ export const ShogunButton = (() => {
1292
1481
  authView === "import" && renderImportForm(),
1293
1482
  authView === "webauthn-username" &&
1294
1483
  renderWebAuthnUsernameForm(),
1484
+ authView === "webauthn-recovery" &&
1485
+ renderWebauthnRecoveryForm(),
1295
1486
  authView === "webauthn-signup-result" &&
1296
1487
  renderWebauthnSignupResult(),
1297
- authView === "zkproof-login" && renderZkProofLoginForm()))))));
1488
+ authView === "zkproof-login" && renderZkProofLoginForm(),
1489
+ authView === "zkproof-signup-result" &&
1490
+ renderZkProofSignupResult()))))));
1298
1491
  };
1299
1492
  Button.displayName = "ShogunButton";
1300
1493
  return Object.assign(Button, {