shogun-button-react 6.4.0 β†’ 6.4.1

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
@@ -220,7 +220,7 @@ export function ShogunButtonProvider({ children, core, options, onLoginSuccess,
220
220
  };
221
221
  // Unified signup
222
222
  const signUp = async (method, ...args) => {
223
- var _a, _b;
223
+ var _a, _b, _c;
224
224
  try {
225
225
  if (!core) {
226
226
  throw new Error("SDK not initialized");
@@ -252,18 +252,39 @@ export function ShogunButtonProvider({ children, core, options, onLoginSuccess,
252
252
  throw error;
253
253
  }
254
254
  break;
255
- case "webauthn":
256
- username = args[0];
255
+ case "webauthn": {
256
+ username = typeof args[0] === "string" ? args[0].trim() : "";
257
+ const webauthnOptions = args.length > 1 && typeof args[1] === "object" && args[1] !== null
258
+ ? args[1]
259
+ : {};
260
+ if (!username) {
261
+ throw new Error("Username is required for WebAuthn registration");
262
+ }
257
263
  if (isShogunCore(core)) {
258
264
  const webauthn = core.getPlugin("webauthn");
259
265
  if (!webauthn)
260
266
  throw new Error("WebAuthn plugin not available");
261
- result = await webauthn.signUp(username, { generateSeedPhrase: true });
267
+ const pluginOptions = {};
268
+ if (webauthnOptions.seedPhrase) {
269
+ pluginOptions.seedPhrase = webauthnOptions.seedPhrase.trim();
270
+ pluginOptions.generateSeedPhrase =
271
+ (_a = webauthnOptions.generateSeedPhrase) !== null && _a !== void 0 ? _a : false;
272
+ }
273
+ else if (typeof webauthnOptions.generateSeedPhrase === "boolean") {
274
+ pluginOptions.generateSeedPhrase =
275
+ webauthnOptions.generateSeedPhrase;
276
+ }
277
+ if (pluginOptions.generateSeedPhrase === undefined &&
278
+ !pluginOptions.seedPhrase) {
279
+ pluginOptions.generateSeedPhrase = true;
280
+ }
281
+ result = await webauthn.signUp(username, pluginOptions);
262
282
  }
263
283
  else {
264
284
  throw new Error("WebAuthn requires ShogunCore");
265
285
  }
266
286
  break;
287
+ }
267
288
  case "web3":
268
289
  if (isShogunCore(core)) {
269
290
  const web3 = core.getPlugin("web3");
@@ -320,7 +341,7 @@ export function ShogunButtonProvider({ children, core, options, onLoginSuccess,
320
341
  if (result.success) {
321
342
  let userPub = result.userPub || "";
322
343
  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) || "";
344
+ userPub = ((_c = (_b = core.gun.user()) === null || _b === void 0 ? void 0 : _b.is) === null || _c === void 0 ? void 0 : _c.pub) || "";
324
345
  }
325
346
  const displayName = result.alias || username || userPub.slice(0, 8) + "...";
326
347
  setIsLoggedIn(true);
@@ -616,7 +637,10 @@ export const ShogunButton = (() => {
616
637
  const [showCopySuccess, setShowCopySuccess] = useState(false);
617
638
  const [showImportSuccess, setShowImportSuccess] = useState(false);
618
639
  const [zkTrapdoor, setZkTrapdoor] = useState("");
640
+ const [zkSignupTrapdoor, setZkSignupTrapdoor] = useState("");
641
+ const [showZkTrapdoorCopySuccess, setShowZkTrapdoorCopySuccess] = useState(false);
619
642
  const [webauthnSeedPhrase, setWebauthnSeedPhrase] = useState("");
643
+ const [webauthnRecoverySeed, setWebauthnRecoverySeed] = useState("");
620
644
  const dropdownRef = useRef(null);
621
645
  // Handle click outside to close dropdown
622
646
  useEffect(() => {
@@ -753,6 +777,41 @@ export const ShogunButton = (() => {
753
777
  }
754
778
  setAuthView("webauthn-username");
755
779
  };
780
+ const handleWebauthnImport = async () => {
781
+ setError("");
782
+ setLoading(true);
783
+ try {
784
+ const username = formUsername.trim();
785
+ const recoveryCode = webauthnRecoverySeed.trim();
786
+ if (!username) {
787
+ throw new Error("Please enter your username");
788
+ }
789
+ if (!recoveryCode) {
790
+ throw new Error("Please enter your recovery code");
791
+ }
792
+ if (!isShogunCore(core)) {
793
+ throw new Error("WebAuthn recovery requires ShogunCore");
794
+ }
795
+ const result = await signUp("webauthn", username, {
796
+ seedPhrase: recoveryCode,
797
+ generateSeedPhrase: false,
798
+ });
799
+ if (!result || !result.success) {
800
+ throw new Error((result === null || result === void 0 ? void 0 : result.error) || "Failed to restore account");
801
+ }
802
+ const seedToDisplay = result.seedPhrase || recoveryCode;
803
+ setWebauthnSeedPhrase(seedToDisplay);
804
+ setWebauthnRecoverySeed("");
805
+ setShowCopySuccess(false);
806
+ setAuthView("webauthn-signup-result");
807
+ }
808
+ catch (e) {
809
+ setError(e.message || "Failed to restore WebAuthn account");
810
+ }
811
+ finally {
812
+ setLoading(false);
813
+ }
814
+ };
756
815
  const handleZkProofAuth = () => {
757
816
  if (!hasPlugin("zkproof")) {
758
817
  setError("ZK-Proof plugin not available");
@@ -791,8 +850,16 @@ export const ShogunButton = (() => {
791
850
  if (!result || !result.success) {
792
851
  throw new Error((result === null || result === void 0 ? void 0 : result.error) || "ZK-Proof signup failed");
793
852
  }
794
- setAuthView("options");
795
- setModalIsOpen(false);
853
+ const trapdoorValue = result.seedPhrase || result.trapdoor || "";
854
+ if (trapdoorValue) {
855
+ setZkSignupTrapdoor(trapdoorValue);
856
+ setShowZkTrapdoorCopySuccess(false);
857
+ setAuthView("zkproof-signup-result");
858
+ }
859
+ else {
860
+ setAuthView("options");
861
+ setModalIsOpen(false);
862
+ }
796
863
  }
797
864
  catch (e) {
798
865
  setError(e.message || "ZK-Proof signup failed");
@@ -911,7 +978,10 @@ export const ShogunButton = (() => {
911
978
  setShowImportSuccess(false);
912
979
  setRecoveredHint("");
913
980
  setZkTrapdoor("");
981
+ setZkSignupTrapdoor("");
982
+ setShowZkTrapdoorCopySuccess(false);
914
983
  setWebauthnSeedPhrase("");
984
+ setWebauthnRecoverySeed("");
915
985
  };
916
986
  const openModal = () => {
917
987
  resetForm();
@@ -1037,7 +1107,52 @@ export const ShogunButton = (() => {
1037
1107
  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
1108
  React.createElement("button", { type: "button", className: "shogun-submit-button", onClick: () => handleAuth("webauthn", formUsername), disabled: loading || !formUsername.trim() }, loading ? "Processing..." : `Continue with WebAuthn`),
1039
1109
  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"))));
1110
+ React.createElement("button", { type: "button", className: "shogun-back-button", onClick: () => setAuthView("options"), disabled: loading }, "\u2190 Back to Options"),
1111
+ formMode === "login" && (React.createElement("button", { type: "button", className: "shogun-toggle-mode", onClick: () => setAuthView("webauthn-recovery"), disabled: loading }, "Restore with Recovery Code")))));
1112
+ const renderWebauthnRecoveryForm = () => (React.createElement("div", { className: "shogun-auth-form" },
1113
+ React.createElement("h3", null, "Restore WebAuthn Account"),
1114
+ React.createElement("div", { style: {
1115
+ backgroundColor: "#fef3c7",
1116
+ padding: "12px",
1117
+ borderRadius: "8px",
1118
+ marginBottom: "16px",
1119
+ border: "1px solid #f59e0b",
1120
+ } },
1121
+ React.createElement("p", { style: {
1122
+ fontSize: "14px",
1123
+ color: "#92400e",
1124
+ margin: "0",
1125
+ fontWeight: "500",
1126
+ } }, "\u26A0\uFE0F Recovery Required"),
1127
+ React.createElement("p", { style: {
1128
+ fontSize: "13px",
1129
+ color: "#a16207",
1130
+ margin: "4px 0 0 0",
1131
+ } }, "Enter the username and recovery code saved during signup to restore access on this device.")),
1132
+ React.createElement("div", { className: "shogun-form-group" },
1133
+ React.createElement("label", { htmlFor: "recoveryUsername" },
1134
+ React.createElement(UserIcon, null),
1135
+ React.createElement("span", null, "Username")),
1136
+ React.createElement("input", { type: "text", id: "recoveryUsername", value: formUsername, onChange: (e) => setFormUsername(e.target.value), disabled: loading, placeholder: "Enter your username", autoFocus: true })),
1137
+ React.createElement("div", { className: "shogun-form-group" },
1138
+ React.createElement("label", { htmlFor: "recoverySeed" },
1139
+ React.createElement(KeyIcon, null),
1140
+ React.createElement("span", null, "Recovery Code")),
1141
+ React.createElement("textarea", { id: "recoverySeed", value: webauthnRecoverySeed, onChange: (e) => setWebauthnRecoverySeed(e.target.value), disabled: loading, placeholder: "Enter your WebAuthn seed phrase...", rows: 4, style: {
1142
+ fontFamily: "monospace",
1143
+ fontSize: "12px",
1144
+ width: "100%",
1145
+ padding: "8px",
1146
+ border: "1px solid #f59e0b",
1147
+ borderRadius: "4px",
1148
+ backgroundColor: "#fffbeb",
1149
+ } })),
1150
+ React.createElement("button", { type: "button", className: "shogun-submit-button", onClick: handleWebauthnImport, disabled: loading }, loading ? "Restoring..." : "Restore Account"),
1151
+ React.createElement("div", { className: "shogun-form-footer" },
1152
+ React.createElement("button", { type: "button", className: "shogun-back-button", onClick: () => {
1153
+ setError("");
1154
+ setAuthView("webauthn-username");
1155
+ }, disabled: loading }, "\u2190 Back to WebAuthn"))));
1041
1156
  const renderRecoveryForm = () => (React.createElement("div", { className: "shogun-auth-form" },
1042
1157
  React.createElement("div", { className: "shogun-form-group" },
1043
1158
  React.createElement("label", { htmlFor: "username" },
@@ -1150,6 +1265,55 @@ export const ShogunButton = (() => {
1150
1265
  React.createElement("button", { type: "button", className: "shogun-submit-button", onClick: handleZkProofLogin, disabled: loading || !zkTrapdoor.trim() }, loading ? "Processing..." : "Login Anonymously"),
1151
1266
  React.createElement("div", { className: "shogun-form-footer" },
1152
1267
  React.createElement("button", { className: "shogun-toggle-mode", onClick: () => setAuthView("options"), disabled: loading }, "Back to Login Options"))));
1268
+ const renderZkProofSignupResult = () => (React.createElement("div", { className: "shogun-auth-form" },
1269
+ React.createElement("h3", null, "ZK-Proof Account Created!"),
1270
+ React.createElement("div", { style: {
1271
+ backgroundColor: "#fef3c7",
1272
+ padding: "12px",
1273
+ borderRadius: "8px",
1274
+ marginBottom: "16px",
1275
+ border: "1px solid #f59e0b",
1276
+ } },
1277
+ React.createElement("p", { style: {
1278
+ fontSize: "14px",
1279
+ color: "#92400e",
1280
+ margin: "0",
1281
+ fontWeight: "500",
1282
+ } }, "\u26A0\uFE0F Important: Save Your Trapdoor"),
1283
+ 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.")),
1284
+ React.createElement("div", { className: "shogun-form-group" },
1285
+ React.createElement("label", null, "Your Trapdoor (Recovery Phrase):"),
1286
+ React.createElement("textarea", { value: zkSignupTrapdoor, readOnly: true, rows: 4, style: {
1287
+ fontFamily: "monospace",
1288
+ fontSize: "12px",
1289
+ width: "100%",
1290
+ padding: "8px",
1291
+ border: "2px solid #f59e0b",
1292
+ borderRadius: "4px",
1293
+ backgroundColor: "#fffbeb",
1294
+ } }),
1295
+ React.createElement("button", { type: "button", className: "shogun-submit-button", style: { marginTop: "8px" }, onClick: async () => {
1296
+ if (!zkSignupTrapdoor) {
1297
+ return;
1298
+ }
1299
+ try {
1300
+ if (navigator.clipboard) {
1301
+ await navigator.clipboard.writeText(zkSignupTrapdoor);
1302
+ setShowZkTrapdoorCopySuccess(true);
1303
+ setTimeout(() => setShowZkTrapdoorCopySuccess(false), 3000);
1304
+ }
1305
+ }
1306
+ catch (copyError) {
1307
+ console.warn("Failed to copy trapdoor:", copyError);
1308
+ }
1309
+ }, disabled: !zkSignupTrapdoor }, "Copy Trapdoor"),
1310
+ showZkTrapdoorCopySuccess && (React.createElement("p", { style: {
1311
+ color: "#047857",
1312
+ fontSize: "12px",
1313
+ marginTop: "6px",
1314
+ } }, "Trapdoor copied to clipboard!"))),
1315
+ React.createElement("div", { className: "shogun-form-footer" },
1316
+ React.createElement("button", { type: "button", className: "shogun-submit-button", onClick: finalizeZkProofSignup }, "I Saved My Trapdoor"))));
1153
1317
  const renderWebauthnSignupResult = () => (React.createElement("div", { className: "shogun-auth-form" },
1154
1318
  React.createElement("h3", null, "WebAuthn Account Created!"),
1155
1319
  React.createElement("div", { style: {
@@ -1292,9 +1456,13 @@ export const ShogunButton = (() => {
1292
1456
  authView === "import" && renderImportForm(),
1293
1457
  authView === "webauthn-username" &&
1294
1458
  renderWebAuthnUsernameForm(),
1459
+ authView === "webauthn-recovery" &&
1460
+ renderWebauthnRecoveryForm(),
1295
1461
  authView === "webauthn-signup-result" &&
1296
1462
  renderWebauthnSignupResult(),
1297
- authView === "zkproof-login" && renderZkProofLoginForm()))))));
1463
+ authView === "zkproof-login" && renderZkProofLoginForm(),
1464
+ authView === "zkproof-signup-result" &&
1465
+ renderZkProofSignupResult()))))));
1298
1466
  };
1299
1467
  Button.displayName = "ShogunButton";
1300
1468
  return Object.assign(Button, {