decane-connect-kit 2.0.0 → 2.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +527 -40
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +63 -2
- package/dist/index.d.ts +63 -2
- package/dist/index.js +526 -40
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -22,6 +22,7 @@ __export(errors_exports, {
|
|
|
22
22
|
NewDeviceError: () => NewDeviceError,
|
|
23
23
|
OriginNotAllowedError: () => OriginNotAllowedError,
|
|
24
24
|
PromptPinNotConfiguredError: () => PromptPinNotConfiguredError,
|
|
25
|
+
ReconnectRequiredError: () => ReconnectRequiredError,
|
|
25
26
|
RecoveryFailedError: () => RecoveryFailedError,
|
|
26
27
|
RecoveryWalletMismatchError: () => RecoveryWalletMismatchError,
|
|
27
28
|
SessionExpiredError: () => SessionExpiredError,
|
|
@@ -31,7 +32,7 @@ __export(errors_exports, {
|
|
|
31
32
|
WebAuthnNotSupportedError: () => WebAuthnNotSupportedError,
|
|
32
33
|
WrongPasswordError: () => WrongPasswordError
|
|
33
34
|
});
|
|
34
|
-
var DecaneConnectError, InvalidApiKeyError, OriginNotAllowedError, SessionExpiredError, UnlockRequiredError, UserCancelledError, WebAuthnNotSupportedError, InvalidPinError, CallSequenceError, NetworkError, ShareCorruptedError, NewDeviceError, PromptPinNotConfiguredError, AttestationError, RecoveryFailedError, InvalidRecoveryFileError, WrongPasswordError, RecoveryWalletMismatchError, ExportNotAuthorizedError;
|
|
35
|
+
var DecaneConnectError, InvalidApiKeyError, OriginNotAllowedError, SessionExpiredError, UnlockRequiredError, ReconnectRequiredError, UserCancelledError, WebAuthnNotSupportedError, InvalidPinError, CallSequenceError, NetworkError, ShareCorruptedError, NewDeviceError, PromptPinNotConfiguredError, AttestationError, RecoveryFailedError, InvalidRecoveryFileError, WrongPasswordError, RecoveryWalletMismatchError, ExportNotAuthorizedError;
|
|
35
36
|
var init_errors = __esm({
|
|
36
37
|
"src/social/errors.ts"() {
|
|
37
38
|
"use strict";
|
|
@@ -63,6 +64,15 @@ var init_errors = __esm({
|
|
|
63
64
|
super("UNLOCK_REQUIRED", "Wallet must be unlocked before signing", true);
|
|
64
65
|
}
|
|
65
66
|
};
|
|
67
|
+
ReconnectRequiredError = class extends DecaneConnectError {
|
|
68
|
+
constructor() {
|
|
69
|
+
super(
|
|
70
|
+
"RECONNECT_REQUIRED",
|
|
71
|
+
"Your session ended \u2014 sign in again to unlock this wallet",
|
|
72
|
+
true
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
66
76
|
UserCancelledError = class extends DecaneConnectError {
|
|
67
77
|
constructor() {
|
|
68
78
|
super("USER_CANCELLED", "User cancelled the operation", true);
|
|
@@ -2162,6 +2172,13 @@ var TeeClient = class {
|
|
|
2162
2172
|
await verifyTeeAttestation(this.teeUrl, nonceHex);
|
|
2163
2173
|
this.attested = true;
|
|
2164
2174
|
}
|
|
2175
|
+
/**
|
|
2176
|
+
* Opens a signing session and returns the addresses the TEE derived from the
|
|
2177
|
+
* key it actually reconstructed — authoritative, unlike the backend's
|
|
2178
|
+
* recorded copy, which nothing else can verify. Returns null if the TEE
|
|
2179
|
+
* couldn't derive them (older signer build, or a transient share-fetch
|
|
2180
|
+
* failure); callers fall back to the backend's addresses in that case.
|
|
2181
|
+
*/
|
|
2165
2182
|
async openSession(deviceShare, jwt) {
|
|
2166
2183
|
await this.ensureAttested();
|
|
2167
2184
|
const res = await fetch(`${this.teeUrl}/signer/session`, {
|
|
@@ -2170,7 +2187,9 @@ var TeeClient = class {
|
|
|
2170
2187
|
body: JSON.stringify({ deviceShare: bytesToBase64(deviceShare) })
|
|
2171
2188
|
});
|
|
2172
2189
|
if (!res.ok) throw new Error("session open failed");
|
|
2173
|
-
|
|
2190
|
+
const body = await res.json();
|
|
2191
|
+
this.handle = body.handle;
|
|
2192
|
+
return body.addresses ?? null;
|
|
2174
2193
|
}
|
|
2175
2194
|
/**
|
|
2176
2195
|
* Flow B: exchange a recovery share (from a recovery-share file) for a
|
|
@@ -2303,9 +2322,24 @@ var SessionManager = class {
|
|
|
2303
2322
|
window.addEventListener("pagehide", () => this.end());
|
|
2304
2323
|
}
|
|
2305
2324
|
}
|
|
2306
|
-
|
|
2325
|
+
/**
|
|
2326
|
+
* Opens the TEE session and resolves which addresses this wallet really has.
|
|
2327
|
+
*
|
|
2328
|
+
* `fallback` is whatever the backend recorded; the TEE's answer is derived
|
|
2329
|
+
* from the signing key itself and therefore outranks it. Returns the
|
|
2330
|
+
* resolved addresses so callers can propagate the corrected values rather
|
|
2331
|
+
* than continuing to use a stale record.
|
|
2332
|
+
*/
|
|
2333
|
+
async start(deviceShare, jwt, fallback) {
|
|
2307
2334
|
this.tee = new TeeClient(this.teeUrl);
|
|
2308
|
-
await this.tee.openSession(deviceShare, jwt);
|
|
2335
|
+
const verified = await this.tee.openSession(deviceShare, jwt);
|
|
2336
|
+
const addresses = verified ?? fallback;
|
|
2337
|
+
if (!addresses) {
|
|
2338
|
+
this.tee = null;
|
|
2339
|
+
throw new Error(
|
|
2340
|
+
"No addresses available for this wallet \u2014 the signer could not derive them and none are recorded"
|
|
2341
|
+
);
|
|
2342
|
+
}
|
|
2309
2343
|
this.evmAddress = addresses.evm;
|
|
2310
2344
|
this.solanaAddress = addresses.solana;
|
|
2311
2345
|
this.expiresAt = Date.now() + this.durationMs;
|
|
@@ -2315,6 +2349,7 @@ var SessionManager = class {
|
|
|
2315
2349
|
this.events.emit("session-expired");
|
|
2316
2350
|
}, this.durationMs);
|
|
2317
2351
|
this.events.emit("session-started");
|
|
2352
|
+
return addresses;
|
|
2318
2353
|
}
|
|
2319
2354
|
end() {
|
|
2320
2355
|
void this.tee?.closeSession();
|
|
@@ -2365,6 +2400,11 @@ async function splitMaster(master, version = 1) {
|
|
|
2365
2400
|
if (parts.length !== 3) throw new Error("unexpected share count");
|
|
2366
2401
|
return { device: parts[0], server: parts[1], recovery: parts[2], version };
|
|
2367
2402
|
}
|
|
2403
|
+
async function combineShares(a, b) {
|
|
2404
|
+
const master = await combine([a, b]);
|
|
2405
|
+
if (master.length !== 32) throw new Error("reconstruction produced wrong length");
|
|
2406
|
+
return master;
|
|
2407
|
+
}
|
|
2368
2408
|
|
|
2369
2409
|
// src/social/crypto/wrap.ts
|
|
2370
2410
|
function buf(u) {
|
|
@@ -2809,6 +2849,18 @@ async function createDecaneConnect(config) {
|
|
|
2809
2849
|
}
|
|
2810
2850
|
};
|
|
2811
2851
|
}
|
|
2852
|
+
async function persistRotatedDeviceShare(userId, deviceShare) {
|
|
2853
|
+
const stored = await storage.getShare(userId);
|
|
2854
|
+
if (!stored) {
|
|
2855
|
+
throw new CallSequenceError(
|
|
2856
|
+
"Share set was rotated but this device has no stored share to update. Recover the wallet on this device before rotating."
|
|
2857
|
+
);
|
|
2858
|
+
}
|
|
2859
|
+
const wrappingKey = await getWrappingKey(stored);
|
|
2860
|
+
const iv = generateRandomBytes(12);
|
|
2861
|
+
const wrappedDeviceShare = await aesGcmEncrypt(wrappingKey, deviceShare, iv);
|
|
2862
|
+
await storage.putShare({ ...stored, wrappedDeviceShare, iv });
|
|
2863
|
+
}
|
|
2812
2864
|
async function getWrappingKey(stored) {
|
|
2813
2865
|
if (stored.unlockMethod === "passkey") {
|
|
2814
2866
|
const prfOutput = await getPrfOutput({
|
|
@@ -2944,8 +2996,15 @@ async function createDecaneConnect(config) {
|
|
|
2944
2996
|
console.warn("decane-connect-kit: wrapped share backup failed; user can retry from settings.", e);
|
|
2945
2997
|
}
|
|
2946
2998
|
}
|
|
2947
|
-
await session.start(shareSet.device, jwt, addresses);
|
|
2999
|
+
const confirmed = await session.start(shareSet.device, jwt, addresses);
|
|
2948
3000
|
zeroize(shareSet.device);
|
|
3001
|
+
if (confirmed.evm.toLowerCase() !== addresses.evm.toLowerCase()) {
|
|
3002
|
+
console.warn(
|
|
3003
|
+
`decane-connect-kit: signer derived a different EVM address (${confirmed.evm}) than this device did (${addresses.evm}) during setup. Using the signer's.`
|
|
3004
|
+
);
|
|
3005
|
+
}
|
|
3006
|
+
addresses.evm = evmAddress = confirmed.evm;
|
|
3007
|
+
addresses.solana = solanaAddress = confirmed.solana;
|
|
2949
3008
|
if (config.offerRecoveryAtSignup !== false && config.onRecoveryShareOffer) {
|
|
2950
3009
|
try {
|
|
2951
3010
|
const choice = await config.onRecoveryShareOffer({ evmAddress, solanaAddress });
|
|
@@ -2969,15 +3028,15 @@ async function createDecaneConnect(config) {
|
|
|
2969
3028
|
}
|
|
2970
3029
|
async function returningUserUnlock(userId, jwt) {
|
|
2971
3030
|
const rawAddresses = await api.getAddresses(jwt);
|
|
2972
|
-
|
|
2973
|
-
throw new CallSequenceError(
|
|
2974
|
-
"No addresses are recorded for this user \u2014 inconsistent server state"
|
|
2975
|
-
);
|
|
2976
|
-
}
|
|
2977
|
-
const addresses = { evm: rawAddresses.evm, solana: rawAddresses.solana };
|
|
3031
|
+
const recordedAddresses = rawAddresses.evm && rawAddresses.solana ? { evm: rawAddresses.evm, solana: rawAddresses.solana } : null;
|
|
2978
3032
|
const stored = await storage.getShare(userId);
|
|
2979
3033
|
let deviceShare;
|
|
2980
3034
|
if (!stored) {
|
|
3035
|
+
if (!config.onRecoveryRotated) {
|
|
3036
|
+
throw new CallSequenceError(
|
|
3037
|
+
"Recovery rotates the share set and issues a new recovery file, but no onRecoveryRotated callback is configured to save it. Configure onRecoveryRotated before allowing recovery."
|
|
3038
|
+
);
|
|
3039
|
+
}
|
|
2981
3040
|
deviceShare = await recoverDeviceShare({
|
|
2982
3041
|
userId,
|
|
2983
3042
|
jwt,
|
|
@@ -2988,13 +3047,11 @@ async function createDecaneConnect(config) {
|
|
|
2988
3047
|
onProgress: config.onRecoveryProgress,
|
|
2989
3048
|
promptForRecoveryFile: config.promptForRecoveryFile,
|
|
2990
3049
|
onNewRecoveryShare: async (recoveryShare, shareSetVersion) => {
|
|
2991
|
-
if (!config.onRecoveryRotated)
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
const file = await buildRecoveryShareFile(recoveryShare, password, addresses, shareSetVersion, passwordHint);
|
|
2997
|
-
await config.onRecoveryFileReady?.(file, recoveryShareFileName(addresses.evm));
|
|
3050
|
+
if (!config.onRecoveryRotated) return;
|
|
3051
|
+
if (!recordedAddresses) return;
|
|
3052
|
+
const { password, passwordHint } = await config.onRecoveryRotated({ evmAddress: recordedAddresses.evm, solanaAddress: recordedAddresses.solana });
|
|
3053
|
+
const file = await buildRecoveryShareFile(recoveryShare, password, recordedAddresses, shareSetVersion, passwordHint);
|
|
3054
|
+
await config.onRecoveryFileReady?.(file, recoveryShareFileName(recordedAddresses.evm));
|
|
2998
3055
|
await api.markRecoveryShareSaved(jwt);
|
|
2999
3056
|
}
|
|
3000
3057
|
});
|
|
@@ -3013,8 +3070,13 @@ async function createDecaneConnect(config) {
|
|
|
3013
3070
|
throw stored.unlockMethod === "pin" ? new InvalidPinError() : new ShareCorruptedError();
|
|
3014
3071
|
}
|
|
3015
3072
|
}
|
|
3016
|
-
await session.start(deviceShare, jwt,
|
|
3073
|
+
const addresses = await session.start(deviceShare, jwt, recordedAddresses);
|
|
3017
3074
|
zeroize(deviceShare);
|
|
3075
|
+
if (recordedAddresses && recordedAddresses.evm.toLowerCase() !== addresses.evm.toLowerCase()) {
|
|
3076
|
+
console.warn(
|
|
3077
|
+
`decane-connect-kit: recorded EVM address (${recordedAddresses.evm}) does not match the signing key (${addresses.evm}). Using the signing key's address; the record has been corrected.`
|
|
3078
|
+
);
|
|
3079
|
+
}
|
|
3018
3080
|
return addresses;
|
|
3019
3081
|
}
|
|
3020
3082
|
async function handleAuthResult(result) {
|
|
@@ -3076,14 +3138,24 @@ async function createDecaneConnect(config) {
|
|
|
3076
3138
|
return handleAuthResult(result);
|
|
3077
3139
|
},
|
|
3078
3140
|
async unlock() {
|
|
3079
|
-
if (!auth)
|
|
3080
|
-
throw new CallSequenceError("Not connected \u2014 call connect() first");
|
|
3141
|
+
if (!auth) {
|
|
3142
|
+
throw persisted ? new ReconnectRequiredError() : new CallSequenceError("Not connected \u2014 call connect() first");
|
|
3143
|
+
}
|
|
3081
3144
|
if (session.isActive()) return;
|
|
3082
3145
|
await returningUserUnlock(auth.userId, auth.jwt);
|
|
3083
3146
|
},
|
|
3084
3147
|
isUnlocked() {
|
|
3085
3148
|
return session.isActive();
|
|
3086
3149
|
},
|
|
3150
|
+
/**
|
|
3151
|
+
* True when we have a persisted identity but no live auth state, i.e.
|
|
3152
|
+
* unlock() cannot succeed and the user must sign in again. Exposed on the
|
|
3153
|
+
* client (not just the React context) so non-React consumers can branch on
|
|
3154
|
+
* it without having to catch ReconnectRequiredError.
|
|
3155
|
+
*/
|
|
3156
|
+
needsReconnect() {
|
|
3157
|
+
return !auth && !!persisted;
|
|
3158
|
+
},
|
|
3087
3159
|
sessionExpiresAt() {
|
|
3088
3160
|
return session.expiresAtTimestamp();
|
|
3089
3161
|
},
|
|
@@ -3176,10 +3248,12 @@ async function createDecaneConnect(config) {
|
|
|
3176
3248
|
const tee = session.requireActive();
|
|
3177
3249
|
const result = await tee.rotateShares(auth.jwt);
|
|
3178
3250
|
try {
|
|
3251
|
+
await persistRotatedDeviceShare(auth.userId, result.deviceShare);
|
|
3252
|
+
const addresses = await deriveAddressesFromShares(result.deviceShare, result.recoveryShare);
|
|
3179
3253
|
const file = await buildRecoveryShareFile(
|
|
3180
3254
|
result.recoveryShare,
|
|
3181
3255
|
opts.password,
|
|
3182
|
-
|
|
3256
|
+
addresses,
|
|
3183
3257
|
result.shareSetVersion,
|
|
3184
3258
|
opts.passwordHint
|
|
3185
3259
|
);
|
|
@@ -3195,11 +3269,13 @@ async function createDecaneConnect(config) {
|
|
|
3195
3269
|
const tee = session.requireActive();
|
|
3196
3270
|
const result = await tee.exportPortableBackup(auth.jwt);
|
|
3197
3271
|
try {
|
|
3272
|
+
await persistRotatedDeviceShare(auth.userId, result.deviceShare);
|
|
3273
|
+
const addresses = await deriveAddressesFromShares(result.deviceShare, result.recoveryShare);
|
|
3198
3274
|
return await buildPortableBackup(
|
|
3199
3275
|
result.deviceShare,
|
|
3200
3276
|
result.recoveryShare,
|
|
3201
3277
|
opts.password,
|
|
3202
|
-
|
|
3278
|
+
addresses,
|
|
3203
3279
|
result.shareSetVersion,
|
|
3204
3280
|
opts.passwordHint
|
|
3205
3281
|
);
|
|
@@ -3271,6 +3347,17 @@ function b64uToBuffer(b64u) {
|
|
|
3271
3347
|
function toHex(bytes) {
|
|
3272
3348
|
return "0x" + Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
3273
3349
|
}
|
|
3350
|
+
async function deriveAddressesFromShares(a, b) {
|
|
3351
|
+
const master = await combineShares(a, b);
|
|
3352
|
+
try {
|
|
3353
|
+
return {
|
|
3354
|
+
evm: new Wallet(toHex(master)).address,
|
|
3355
|
+
solana: Keypair.fromSeed(master).publicKey.toBase58()
|
|
3356
|
+
};
|
|
3357
|
+
} finally {
|
|
3358
|
+
zeroize(master);
|
|
3359
|
+
}
|
|
3360
|
+
}
|
|
3274
3361
|
|
|
3275
3362
|
// src/social/index.ts
|
|
3276
3363
|
init_errors();
|
|
@@ -3432,7 +3519,7 @@ function WalletIcon({ src, name, size = 36 }) {
|
|
|
3432
3519
|
fontWeight: 800,
|
|
3433
3520
|
color: "var(--dc-accent)",
|
|
3434
3521
|
flexShrink: 0,
|
|
3435
|
-
fontFamily: "var(--dc-font-sans)",
|
|
3522
|
+
fontFamily: "var(--dc-font-sans, system-ui, sans-serif)",
|
|
3436
3523
|
letterSpacing: "-0.02em"
|
|
3437
3524
|
}
|
|
3438
3525
|
},
|
|
@@ -3777,7 +3864,10 @@ function WalletSelector({
|
|
|
3777
3864
|
alignItems: "center",
|
|
3778
3865
|
justifyContent: "center",
|
|
3779
3866
|
padding: "16px",
|
|
3780
|
-
|
|
3867
|
+
// Same fallback chain as SocialWalletModal — without it, this modal
|
|
3868
|
+
// drops to the browser default serif whenever the token block hasn't
|
|
3869
|
+
// been injected, while the social modal still lands on system-ui.
|
|
3870
|
+
fontFamily: "var(--dc-font-sans, system-ui, sans-serif)"
|
|
3781
3871
|
}
|
|
3782
3872
|
},
|
|
3783
3873
|
/* @__PURE__ */ React2.createElement(
|
|
@@ -4010,7 +4100,7 @@ function WalletSelector({
|
|
|
4010
4100
|
fontSize: 13,
|
|
4011
4101
|
padding: "8px 12px 8px 32px",
|
|
4012
4102
|
outline: "none",
|
|
4013
|
-
fontFamily: "var(--dc-font-sans)",
|
|
4103
|
+
fontFamily: "var(--dc-font-sans, system-ui, sans-serif)",
|
|
4014
4104
|
transition: "border-color 0.15s",
|
|
4015
4105
|
boxSizing: "border-box"
|
|
4016
4106
|
},
|
|
@@ -4196,6 +4286,15 @@ function countdown(expiresAt) {
|
|
|
4196
4286
|
const secs = Math.max(0, Math.round((expiresAt - Date.now()) / 1e3));
|
|
4197
4287
|
return `${Math.floor(secs / 60)}m ${String(secs % 60).padStart(2, "0")}s`;
|
|
4198
4288
|
}
|
|
4289
|
+
function downloadJson(data, filename) {
|
|
4290
|
+
const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" });
|
|
4291
|
+
const url = URL.createObjectURL(blob);
|
|
4292
|
+
const a = document.createElement("a");
|
|
4293
|
+
a.href = url;
|
|
4294
|
+
a.download = filename;
|
|
4295
|
+
a.click();
|
|
4296
|
+
URL.revokeObjectURL(url);
|
|
4297
|
+
}
|
|
4199
4298
|
function CopyButton({ value }) {
|
|
4200
4299
|
const [copied, setCopied] = useState2(false);
|
|
4201
4300
|
async function copy() {
|
|
@@ -4223,7 +4322,54 @@ function CopyButton({ value }) {
|
|
|
4223
4322
|
copied ? "\u2713" : "copy"
|
|
4224
4323
|
);
|
|
4225
4324
|
}
|
|
4226
|
-
function
|
|
4325
|
+
function useBalance(kind, address, chain, rpcUrls) {
|
|
4326
|
+
const [state, setState] = useState2({ loading: false, value: null, symbol: kind === "evm" ? "ETH" : "SOL" });
|
|
4327
|
+
useEffect2(() => {
|
|
4328
|
+
if (!address || !chain) return;
|
|
4329
|
+
let cancelled = false;
|
|
4330
|
+
setState((s) => ({ ...s, loading: true }));
|
|
4331
|
+
(async () => {
|
|
4332
|
+
try {
|
|
4333
|
+
if (kind === "evm") {
|
|
4334
|
+
const rpcUrl = rpcUrls?.[chain] ?? DEFAULT_RPCS[chain];
|
|
4335
|
+
if (!rpcUrl) {
|
|
4336
|
+
if (!cancelled) setState({ loading: false, value: null, symbol: "ETH" });
|
|
4337
|
+
return;
|
|
4338
|
+
}
|
|
4339
|
+
const { JsonRpcProvider, formatEther } = await import("ethers");
|
|
4340
|
+
const wei = await new JsonRpcProvider(rpcUrl).getBalance(address);
|
|
4341
|
+
if (!cancelled) setState({ loading: false, value: formatEther(wei), symbol: "ETH" });
|
|
4342
|
+
} else {
|
|
4343
|
+
const cluster = chain.slice("solana:".length);
|
|
4344
|
+
const endpoint = rpcUrls?.[chain] ?? (cluster === "devnet" ? "https://api.devnet.solana.com" : "https://api.mainnet-beta.solana.com");
|
|
4345
|
+
const { Connection, PublicKey, LAMPORTS_PER_SOL } = await import("@solana/web3.js");
|
|
4346
|
+
const lamports = await new Connection(endpoint).getBalance(new PublicKey(address));
|
|
4347
|
+
if (!cancelled) setState({ loading: false, value: String(lamports / LAMPORTS_PER_SOL), symbol: "SOL" });
|
|
4348
|
+
}
|
|
4349
|
+
} catch {
|
|
4350
|
+
if (!cancelled) setState({ loading: false, value: null, symbol: kind === "evm" ? "ETH" : "SOL" });
|
|
4351
|
+
}
|
|
4352
|
+
})();
|
|
4353
|
+
return () => {
|
|
4354
|
+
cancelled = true;
|
|
4355
|
+
};
|
|
4356
|
+
}, [kind, address, chain, rpcUrls]);
|
|
4357
|
+
return state;
|
|
4358
|
+
}
|
|
4359
|
+
function formatBalance(value) {
|
|
4360
|
+
if (value == null) return "\u2014";
|
|
4361
|
+
const n = Number(value);
|
|
4362
|
+
if (!Number.isFinite(n)) return value;
|
|
4363
|
+
return n === 0 ? "0" : n < 1e-4 ? "<0.0001" : n.toFixed(4).replace(/\.?0+$/, "");
|
|
4364
|
+
}
|
|
4365
|
+
function AddressRow({
|
|
4366
|
+
label,
|
|
4367
|
+
address,
|
|
4368
|
+
kind,
|
|
4369
|
+
chain,
|
|
4370
|
+
rpcUrls
|
|
4371
|
+
}) {
|
|
4372
|
+
const balance = useBalance(kind, address, chain, rpcUrls);
|
|
4227
4373
|
const short = `${address.slice(0, 8)}\u2026${address.slice(-6)}`;
|
|
4228
4374
|
return /* @__PURE__ */ React3.createElement("div", { style: {
|
|
4229
4375
|
background: "var(--dc-surface)",
|
|
@@ -4231,7 +4377,233 @@ function AddressRow({ label, address }) {
|
|
|
4231
4377
|
borderRadius: 10,
|
|
4232
4378
|
padding: "10px 14px",
|
|
4233
4379
|
marginBottom: 8
|
|
4234
|
-
} }, /* @__PURE__ */ React3.createElement("div", { style: { fontSize: 10, fontWeight: 700, color: "var(--dc-text-muted)", letterSpacing: "0.06em", textTransform: "uppercase",
|
|
4380
|
+
} }, /* @__PURE__ */ React3.createElement("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 5 } }, /* @__PURE__ */ React3.createElement("span", { style: { fontSize: 10, fontWeight: 700, color: "var(--dc-text-muted)", letterSpacing: "0.06em", textTransform: "uppercase" } }, label), /* @__PURE__ */ React3.createElement("span", { style: { fontSize: 11, fontFamily: "monospace", color: "var(--dc-text)" } }, balance.loading ? "\u2026" : `${formatBalance(balance.value)} ${balance.symbol}`)), /* @__PURE__ */ React3.createElement("div", { style: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8 } }, /* @__PURE__ */ React3.createElement("span", { style: { fontSize: 13, fontFamily: "monospace", color: "var(--dc-text)", letterSpacing: "0.02em" } }, short), /* @__PURE__ */ React3.createElement(CopyButton, { value: address })));
|
|
4381
|
+
}
|
|
4382
|
+
function SignInView({
|
|
4383
|
+
onGoogleSignIn,
|
|
4384
|
+
onEmailStart,
|
|
4385
|
+
onEmailVerify,
|
|
4386
|
+
authMethods,
|
|
4387
|
+
resumeHint
|
|
4388
|
+
}) {
|
|
4389
|
+
const [googleLoading, setGoogleLoading] = useState2(false);
|
|
4390
|
+
const [email, setEmail] = useState2("");
|
|
4391
|
+
const [code, setCode] = useState2("");
|
|
4392
|
+
const [emailSent, setEmailSent] = useState2(false);
|
|
4393
|
+
const [emailLoading, setEmailLoading] = useState2(false);
|
|
4394
|
+
const [error, setError] = useState2("");
|
|
4395
|
+
async function handleGoogle() {
|
|
4396
|
+
setError("");
|
|
4397
|
+
setGoogleLoading(true);
|
|
4398
|
+
try {
|
|
4399
|
+
await onGoogleSignIn();
|
|
4400
|
+
} catch (e) {
|
|
4401
|
+
setError(e.message);
|
|
4402
|
+
setGoogleLoading(false);
|
|
4403
|
+
}
|
|
4404
|
+
}
|
|
4405
|
+
async function handleEmailStart(e) {
|
|
4406
|
+
e.preventDefault();
|
|
4407
|
+
setError("");
|
|
4408
|
+
setEmailLoading(true);
|
|
4409
|
+
try {
|
|
4410
|
+
await onEmailStart(email);
|
|
4411
|
+
setEmailSent(true);
|
|
4412
|
+
} catch (e2) {
|
|
4413
|
+
setError(e2.message);
|
|
4414
|
+
} finally {
|
|
4415
|
+
setEmailLoading(false);
|
|
4416
|
+
}
|
|
4417
|
+
}
|
|
4418
|
+
async function handleEmailVerify(e) {
|
|
4419
|
+
e.preventDefault();
|
|
4420
|
+
setError("");
|
|
4421
|
+
setEmailLoading(true);
|
|
4422
|
+
try {
|
|
4423
|
+
await onEmailVerify(email, code);
|
|
4424
|
+
} catch (e2) {
|
|
4425
|
+
setError(e2.message);
|
|
4426
|
+
} finally {
|
|
4427
|
+
setEmailLoading(false);
|
|
4428
|
+
}
|
|
4429
|
+
}
|
|
4430
|
+
const inputStyle = {
|
|
4431
|
+
width: "100%",
|
|
4432
|
+
background: "var(--dc-surface)",
|
|
4433
|
+
border: "1px solid var(--dc-border)",
|
|
4434
|
+
borderRadius: 8,
|
|
4435
|
+
color: "var(--dc-text)",
|
|
4436
|
+
padding: "9px 12px",
|
|
4437
|
+
fontSize: 13,
|
|
4438
|
+
outline: "none",
|
|
4439
|
+
fontFamily: "inherit",
|
|
4440
|
+
boxSizing: "border-box"
|
|
4441
|
+
};
|
|
4442
|
+
return /* @__PURE__ */ React3.createElement("div", { style: { padding: "0 16px 16px" } }, resumeHint && /* @__PURE__ */ React3.createElement("div", { style: { fontSize: 12, color: "var(--dc-text-muted)", marginBottom: 12, textAlign: "center" } }, "Welcome back \u2014 sign in again to resume ", resumeHint), authMethods.includes("google") && /* @__PURE__ */ React3.createElement(
|
|
4443
|
+
"button",
|
|
4444
|
+
{
|
|
4445
|
+
onClick: handleGoogle,
|
|
4446
|
+
disabled: googleLoading,
|
|
4447
|
+
style: {
|
|
4448
|
+
width: "100%",
|
|
4449
|
+
padding: "10px 0",
|
|
4450
|
+
marginBottom: 10,
|
|
4451
|
+
background: "var(--dc-accent)",
|
|
4452
|
+
color: "var(--dc-accent-text)",
|
|
4453
|
+
border: "none",
|
|
4454
|
+
borderRadius: 10,
|
|
4455
|
+
fontSize: 13,
|
|
4456
|
+
fontWeight: 700,
|
|
4457
|
+
cursor: googleLoading ? "not-allowed" : "pointer",
|
|
4458
|
+
opacity: googleLoading ? 0.7 : 1,
|
|
4459
|
+
fontFamily: "inherit"
|
|
4460
|
+
}
|
|
4461
|
+
},
|
|
4462
|
+
googleLoading ? "Redirecting\u2026" : "Continue with Google"
|
|
4463
|
+
), authMethods.includes("email") && /* @__PURE__ */ React3.createElement(React3.Fragment, null, authMethods.includes("google") && /* @__PURE__ */ React3.createElement("div", { style: { textAlign: "center", color: "var(--dc-text-muted)", fontSize: 12, margin: "10px 0" } }, "\u2014 or \u2014"), !emailSent ? /* @__PURE__ */ React3.createElement("form", { onSubmit: handleEmailStart, style: { display: "flex", gap: 8 } }, /* @__PURE__ */ React3.createElement(
|
|
4464
|
+
"input",
|
|
4465
|
+
{
|
|
4466
|
+
type: "email",
|
|
4467
|
+
required: true,
|
|
4468
|
+
placeholder: "your@email.com",
|
|
4469
|
+
value: email,
|
|
4470
|
+
onChange: (e) => setEmail(e.target.value),
|
|
4471
|
+
style: { ...inputStyle, flex: 1 }
|
|
4472
|
+
}
|
|
4473
|
+
), /* @__PURE__ */ React3.createElement("button", { type: "submit", disabled: emailLoading, style: {
|
|
4474
|
+
padding: "0 16px",
|
|
4475
|
+
background: "var(--dc-surface)",
|
|
4476
|
+
border: "1px solid var(--dc-border)",
|
|
4477
|
+
borderRadius: 8,
|
|
4478
|
+
color: "var(--dc-text)",
|
|
4479
|
+
fontSize: 13,
|
|
4480
|
+
cursor: "pointer",
|
|
4481
|
+
fontFamily: "inherit"
|
|
4482
|
+
} }, emailLoading ? "\u2026" : "Send code")) : /* @__PURE__ */ React3.createElement("form", { onSubmit: handleEmailVerify, style: { display: "flex", gap: 8 } }, /* @__PURE__ */ React3.createElement(
|
|
4483
|
+
"input",
|
|
4484
|
+
{
|
|
4485
|
+
placeholder: "6-digit code",
|
|
4486
|
+
value: code,
|
|
4487
|
+
maxLength: 6,
|
|
4488
|
+
autoFocus: true,
|
|
4489
|
+
onChange: (e) => setCode(e.target.value.replace(/\D/g, "")),
|
|
4490
|
+
style: { ...inputStyle, flex: 1, letterSpacing: 4, textAlign: "center" }
|
|
4491
|
+
}
|
|
4492
|
+
), /* @__PURE__ */ React3.createElement("button", { type: "submit", disabled: emailLoading, style: {
|
|
4493
|
+
padding: "0 16px",
|
|
4494
|
+
background: "var(--dc-accent)",
|
|
4495
|
+
color: "var(--dc-accent-text)",
|
|
4496
|
+
border: "none",
|
|
4497
|
+
borderRadius: 8,
|
|
4498
|
+
fontSize: 13,
|
|
4499
|
+
fontWeight: 700,
|
|
4500
|
+
cursor: "pointer",
|
|
4501
|
+
fontFamily: "inherit"
|
|
4502
|
+
} }, emailLoading ? "\u2026" : "Verify"))), error && /* @__PURE__ */ React3.createElement("div", { style: { marginTop: 10, fontSize: 12, color: "var(--dc-danger, #ff5533)" } }, error));
|
|
4503
|
+
}
|
|
4504
|
+
function RecoverySection({
|
|
4505
|
+
addresses,
|
|
4506
|
+
onRotateShares,
|
|
4507
|
+
onExportPortableBackup
|
|
4508
|
+
}) {
|
|
4509
|
+
const [expanded, setExpanded] = useState2(false);
|
|
4510
|
+
const [mode, setMode] = useState2(null);
|
|
4511
|
+
const [password, setPassword] = useState2("");
|
|
4512
|
+
const [passwordHint, setPasswordHint] = useState2("");
|
|
4513
|
+
const [confirmed, setConfirmed] = useState2(false);
|
|
4514
|
+
const [busy, setBusy] = useState2(false);
|
|
4515
|
+
const [result, setResult] = useState2(null);
|
|
4516
|
+
const [error, setError] = useState2("");
|
|
4517
|
+
function reset() {
|
|
4518
|
+
setMode(null);
|
|
4519
|
+
setPassword("");
|
|
4520
|
+
setPasswordHint("");
|
|
4521
|
+
setConfirmed(false);
|
|
4522
|
+
setError("");
|
|
4523
|
+
}
|
|
4524
|
+
async function handleGenerate() {
|
|
4525
|
+
if (!addresses?.evm || !mode) return;
|
|
4526
|
+
if (password.length < 8) {
|
|
4527
|
+
setError("Password must be at least 8 characters.");
|
|
4528
|
+
return;
|
|
4529
|
+
}
|
|
4530
|
+
if (mode === "portable" && !confirmed) {
|
|
4531
|
+
setError("Confirm you understand the risk first.");
|
|
4532
|
+
return;
|
|
4533
|
+
}
|
|
4534
|
+
setBusy(true);
|
|
4535
|
+
setError("");
|
|
4536
|
+
try {
|
|
4537
|
+
if (mode === "share") {
|
|
4538
|
+
const file = await onRotateShares({ password, passwordHint: passwordHint || void 0 });
|
|
4539
|
+
downloadJson(file, `decane-recovery-${addresses.evm.slice(0, 8)}.json`);
|
|
4540
|
+
setResult({ kind: "share", version: file.shareSetVersion });
|
|
4541
|
+
} else {
|
|
4542
|
+
const file = await onExportPortableBackup({ password, passwordHint: passwordHint || void 0 });
|
|
4543
|
+
downloadJson(file, `decane-full-recovery-${addresses.evm.slice(0, 8)}.json`);
|
|
4544
|
+
setResult({ kind: "portable", version: file.shareSetVersion });
|
|
4545
|
+
}
|
|
4546
|
+
reset();
|
|
4547
|
+
} catch (e) {
|
|
4548
|
+
setError(e.message);
|
|
4549
|
+
} finally {
|
|
4550
|
+
setBusy(false);
|
|
4551
|
+
}
|
|
4552
|
+
}
|
|
4553
|
+
const inputStyle = {
|
|
4554
|
+
width: "100%",
|
|
4555
|
+
background: "var(--dc-bg)",
|
|
4556
|
+
border: "1px solid var(--dc-border)",
|
|
4557
|
+
borderRadius: 8,
|
|
4558
|
+
color: "var(--dc-text)",
|
|
4559
|
+
padding: "8px 10px",
|
|
4560
|
+
fontSize: 12,
|
|
4561
|
+
outline: "none",
|
|
4562
|
+
fontFamily: "inherit",
|
|
4563
|
+
boxSizing: "border-box"
|
|
4564
|
+
};
|
|
4565
|
+
const btnStyle = {
|
|
4566
|
+
padding: "8px 12px",
|
|
4567
|
+
borderRadius: 8,
|
|
4568
|
+
fontSize: 12,
|
|
4569
|
+
fontWeight: 700,
|
|
4570
|
+
cursor: "pointer",
|
|
4571
|
+
fontFamily: "inherit",
|
|
4572
|
+
border: "none"
|
|
4573
|
+
};
|
|
4574
|
+
return /* @__PURE__ */ React3.createElement("div", { style: { margin: "0 16px 14px", border: "1px solid var(--dc-border)", borderRadius: 10, overflow: "hidden" } }, /* @__PURE__ */ React3.createElement(
|
|
4575
|
+
"button",
|
|
4576
|
+
{
|
|
4577
|
+
onClick: () => setExpanded((v) => !v),
|
|
4578
|
+
style: {
|
|
4579
|
+
width: "100%",
|
|
4580
|
+
padding: "10px 14px",
|
|
4581
|
+
background: "var(--dc-surface)",
|
|
4582
|
+
border: "none",
|
|
4583
|
+
display: "flex",
|
|
4584
|
+
alignItems: "center",
|
|
4585
|
+
justifyContent: "space-between",
|
|
4586
|
+
cursor: "pointer",
|
|
4587
|
+
fontFamily: "inherit"
|
|
4588
|
+
}
|
|
4589
|
+
},
|
|
4590
|
+
/* @__PURE__ */ React3.createElement("span", { style: { fontSize: 12, fontWeight: 700, color: "var(--dc-text)" } }, "Recovery"),
|
|
4591
|
+
/* @__PURE__ */ React3.createElement("span", { style: { fontSize: 11, color: "var(--dc-text-muted)" } }, expanded ? "\u25B2" : "\u25BC")
|
|
4592
|
+
), expanded && /* @__PURE__ */ React3.createElement("div", { style: { padding: "12px 14px", borderTop: "1px solid var(--dc-border)" } }, /* @__PURE__ */ React3.createElement("p", { style: { fontSize: 11, color: "var(--dc-text-muted)", lineHeight: 1.5, margin: "0 0 10px" } }, "Your wallet is split into three shares \u2014 device, server, and one that's yours to keep. Any two reconstruct it; one alone reveals nothing."), !mode && /* @__PURE__ */ React3.createElement("div", { style: { display: "flex", flexDirection: "column", gap: 8 } }, /* @__PURE__ */ React3.createElement("button", { onClick: () => {
|
|
4593
|
+
reset();
|
|
4594
|
+
setMode("share");
|
|
4595
|
+
}, style: { ...btnStyle, background: "var(--dc-accent)", color: "var(--dc-accent-text)" } }, "Generate recovery-share file"), /* @__PURE__ */ React3.createElement("button", { onClick: () => {
|
|
4596
|
+
reset();
|
|
4597
|
+
setMode("portable");
|
|
4598
|
+
}, style: { ...btnStyle, background: "transparent", border: "1px solid var(--dc-border)", color: "var(--dc-text)" } }, "Generate full recovery file")), mode === "share" && /* @__PURE__ */ React3.createElement("div", { style: { display: "flex", flexDirection: "column", gap: 8 } }, /* @__PURE__ */ React3.createElement("p", { style: { fontSize: 11, color: "var(--dc-text-muted)", margin: 0 } }, "One share \u2014 useless alone, safe to keep in a password manager or cloud drive. Generating a new one invalidates any previous recovery-share file."), /* @__PURE__ */ React3.createElement("input", { type: "password", placeholder: "Password (min 8 chars)", value: password, onChange: (e) => setPassword(e.target.value), style: inputStyle }), /* @__PURE__ */ React3.createElement("input", { placeholder: "Password hint (optional)", value: passwordHint, onChange: (e) => setPasswordHint(e.target.value), style: inputStyle }), error && /* @__PURE__ */ React3.createElement("div", { style: { fontSize: 11, color: "var(--dc-danger, #ff5533)" } }, error), /* @__PURE__ */ React3.createElement("div", { style: { display: "flex", gap: 8 } }, /* @__PURE__ */ React3.createElement("button", { onClick: handleGenerate, disabled: busy, style: { ...btnStyle, flex: 1, background: "var(--dc-accent)", color: "var(--dc-accent-text)" } }, busy ? "Generating\u2026" : "Generate & download"), /* @__PURE__ */ React3.createElement("button", { onClick: reset, style: { ...btnStyle, background: "transparent", border: "1px solid var(--dc-border)", color: "var(--dc-text-muted)" } }, "Cancel"))), mode === "portable" && /* @__PURE__ */ React3.createElement("div", { style: { display: "flex", flexDirection: "column", gap: 8 } }, /* @__PURE__ */ React3.createElement("div", { style: {
|
|
4599
|
+
background: "rgba(255,85,51,0.08)",
|
|
4600
|
+
border: "1px solid rgba(255,85,51,0.3)",
|
|
4601
|
+
borderRadius: 8,
|
|
4602
|
+
padding: "8px 10px",
|
|
4603
|
+
fontSize: 11,
|
|
4604
|
+
color: "#ff9977",
|
|
4605
|
+
lineHeight: 1.5
|
|
4606
|
+
} }, /* @__PURE__ */ React3.createElement("strong", { style: { display: "block", marginBottom: 3, color: "#ff5533" } }, "This file controls your wallet"), "Contains two shares \u2014 file plus password gives complete control. Keep it offline, not a cloud drive. This also invalidates your current recovery-share file."), /* @__PURE__ */ React3.createElement("label", { style: { display: "flex", alignItems: "center", gap: 6, fontSize: 11, color: "var(--dc-text)" } }, /* @__PURE__ */ React3.createElement("input", { type: "checkbox", checked: confirmed, onChange: (e) => setConfirmed(e.target.checked), style: { width: "auto" } }), "I understand this is a bearer secret"), /* @__PURE__ */ React3.createElement("input", { type: "password", placeholder: "Password (min 8 chars)", value: password, onChange: (e) => setPassword(e.target.value), style: inputStyle }), /* @__PURE__ */ React3.createElement("input", { placeholder: "Password hint (optional)", value: passwordHint, onChange: (e) => setPasswordHint(e.target.value), style: inputStyle }), error && /* @__PURE__ */ React3.createElement("div", { style: { fontSize: 11, color: "var(--dc-danger, #ff5533)" } }, error), /* @__PURE__ */ React3.createElement("div", { style: { display: "flex", gap: 8 } }, /* @__PURE__ */ React3.createElement("button", { onClick: handleGenerate, disabled: busy || !confirmed, style: { ...btnStyle, flex: 1, background: "var(--dc-accent)", color: "var(--dc-accent-text)" } }, busy ? "Generating\u2026" : "Generate & download"), /* @__PURE__ */ React3.createElement("button", { onClick: reset, style: { ...btnStyle, background: "transparent", border: "1px solid var(--dc-border)", color: "var(--dc-text-muted)" } }, "Cancel"))), result && /* @__PURE__ */ React3.createElement("div", { style: { marginTop: 10, fontSize: 11, color: "var(--dc-text-muted)", lineHeight: 1.5 } }, result.kind === "share" ? "Recovery-share file" : "Full recovery file", " downloaded \u2014 share set is now version ", result.version, ". Any previous recovery file no longer works.")));
|
|
4235
4607
|
}
|
|
4236
4608
|
function SocialWalletModal({
|
|
4237
4609
|
open,
|
|
@@ -4239,7 +4611,15 @@ function SocialWalletModal({
|
|
|
4239
4611
|
theme = "auto",
|
|
4240
4612
|
socialAuth,
|
|
4241
4613
|
onUnlock,
|
|
4242
|
-
onDisconnect
|
|
4614
|
+
onDisconnect,
|
|
4615
|
+
onGoogleSignIn,
|
|
4616
|
+
onEmailStart,
|
|
4617
|
+
onEmailVerify,
|
|
4618
|
+
authMethods = ["google", "email"],
|
|
4619
|
+
onRotateShares,
|
|
4620
|
+
onExportPortableBackup,
|
|
4621
|
+
rpcUrls,
|
|
4622
|
+
chains
|
|
4243
4623
|
}) {
|
|
4244
4624
|
const [timer, setTimer] = useState2("");
|
|
4245
4625
|
const [unlocking, setUnlocking] = useState2(false);
|
|
@@ -4288,6 +4668,9 @@ function SocialWalletModal({
|
|
|
4288
4668
|
setDisconnecting(false);
|
|
4289
4669
|
}
|
|
4290
4670
|
}
|
|
4671
|
+
const evmChain = chains?.find((c) => c.startsWith("evm:")) ?? "evm:11155111";
|
|
4672
|
+
const solChain = chains?.find((c) => c.startsWith("solana:")) ?? "solana:mainnet";
|
|
4673
|
+
const showSignIn = !socialAuth.addresses || socialAuth.needsReconnect;
|
|
4291
4674
|
const modal = /* @__PURE__ */ React3.createElement(
|
|
4292
4675
|
"div",
|
|
4293
4676
|
{
|
|
@@ -4325,10 +4708,14 @@ function SocialWalletModal({
|
|
|
4325
4708
|
borderRadius: 20,
|
|
4326
4709
|
width: "100%",
|
|
4327
4710
|
maxWidth: 380,
|
|
4328
|
-
|
|
4329
|
-
|
|
4711
|
+
maxHeight: "90vh",
|
|
4712
|
+
overflowY: "auto",
|
|
4713
|
+
boxShadow: "0 24px 64px rgba(0,0,0,0.6), 0 0 0 1px rgba(255,255,255,0.04)"
|
|
4330
4714
|
} }, /* @__PURE__ */ React3.createElement("div", { style: {
|
|
4331
4715
|
height: 2,
|
|
4716
|
+
position: "sticky",
|
|
4717
|
+
top: 0,
|
|
4718
|
+
zIndex: 1,
|
|
4332
4719
|
background: "linear-gradient(90deg, transparent, var(--dc-accent) 30%, var(--dc-accent) 70%, transparent)"
|
|
4333
4720
|
} }), /* @__PURE__ */ React3.createElement("div", { style: { padding: "18px 20px 16px", display: "flex", alignItems: "center", gap: 10 } }, /* @__PURE__ */ React3.createElement("div", { style: {
|
|
4334
4721
|
width: 34,
|
|
@@ -4357,7 +4744,7 @@ function SocialWalletModal({
|
|
|
4357
4744
|
strokeLinecap: "round",
|
|
4358
4745
|
strokeLinejoin: "round"
|
|
4359
4746
|
}
|
|
4360
|
-
))), /* @__PURE__ */ React3.createElement("div", { style: { flex: 1 } }, /* @__PURE__ */ React3.createElement("div", { style: { fontSize: 15, fontWeight: 800, color: "var(--dc-text)", letterSpacing: "-0.02em" } }, "Social Wallet"), /* @__PURE__ */ React3.createElement("div", { style: { fontSize: 11, color: "var(--dc-text-muted)", marginTop: 1 } }, "Decane non-custodial \xB7 key split
|
|
4747
|
+
))), /* @__PURE__ */ React3.createElement("div", { style: { flex: 1 } }, /* @__PURE__ */ React3.createElement("div", { style: { fontSize: 15, fontWeight: 800, color: "var(--dc-text)", letterSpacing: "-0.02em" } }, "Social Wallet"), /* @__PURE__ */ React3.createElement("div", { style: { fontSize: 11, color: "var(--dc-text-muted)", marginTop: 1 } }, "Decane non-custodial \xB7 key split three ways")), /* @__PURE__ */ React3.createElement(
|
|
4361
4748
|
"button",
|
|
4362
4749
|
{
|
|
4363
4750
|
onClick: onClose,
|
|
@@ -4377,7 +4764,16 @@ function SocialWalletModal({
|
|
|
4377
4764
|
}
|
|
4378
4765
|
},
|
|
4379
4766
|
/* @__PURE__ */ React3.createElement("svg", { width: "12", height: "12", viewBox: "0 0 12 12", fill: "none" }, /* @__PURE__ */ React3.createElement("path", { d: "M1.5 1.5L10.5 10.5M10.5 1.5L1.5 10.5", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round" }))
|
|
4380
|
-
)), /* @__PURE__ */ React3.createElement(
|
|
4767
|
+
)), showSignIn ? /* @__PURE__ */ React3.createElement(
|
|
4768
|
+
SignInView,
|
|
4769
|
+
{
|
|
4770
|
+
onGoogleSignIn,
|
|
4771
|
+
onEmailStart,
|
|
4772
|
+
onEmailVerify,
|
|
4773
|
+
authMethods,
|
|
4774
|
+
resumeHint: socialAuth.needsReconnect ? "your wallet" : void 0
|
|
4775
|
+
}
|
|
4776
|
+
) : /* @__PURE__ */ React3.createElement(React3.Fragment, null, /* @__PURE__ */ React3.createElement("div", { style: {
|
|
4381
4777
|
margin: "0 16px 14px",
|
|
4382
4778
|
padding: "8px 14px",
|
|
4383
4779
|
background: socialAuth.isUnlocked ? "rgba(18,183,106,0.08)" : "rgba(245,200,0,0.08)",
|
|
@@ -4397,7 +4793,14 @@ function SocialWalletModal({
|
|
|
4397
4793
|
fontSize: 12,
|
|
4398
4794
|
fontWeight: 700,
|
|
4399
4795
|
color: socialAuth.isUnlocked ? "#12b76a" : "var(--dc-accent)"
|
|
4400
|
-
} }, socialAuth.isUnlocked ? "Unlocked" : "Locked")), socialAuth.isUnlocked && timer && /* @__PURE__ */ React3.createElement("span", { style: { fontSize: 11, color: "var(--dc-text-muted)", fontFamily: "monospace" } }, "expires ", timer)), /* @__PURE__ */ React3.createElement("div", { style: { padding: "0 16px
|
|
4796
|
+
} }, socialAuth.isUnlocked ? "Unlocked" : "Locked")), socialAuth.isUnlocked && timer && /* @__PURE__ */ React3.createElement("span", { style: { fontSize: 11, color: "var(--dc-text-muted)", fontFamily: "monospace" } }, "expires ", timer)), /* @__PURE__ */ React3.createElement("div", { style: { padding: "0 16px 14px" } }, socialAuth.addresses?.evm && /* @__PURE__ */ React3.createElement(AddressRow, { label: "EVM (Ethereum, Base, \u2026)", address: socialAuth.addresses.evm, kind: "evm", chain: evmChain, rpcUrls }), socialAuth.addresses?.solana && /* @__PURE__ */ React3.createElement(AddressRow, { label: "Solana", address: socialAuth.addresses.solana, kind: "solana", chain: solChain, rpcUrls })), socialAuth.isUnlocked && /* @__PURE__ */ React3.createElement(
|
|
4797
|
+
RecoverySection,
|
|
4798
|
+
{
|
|
4799
|
+
addresses: socialAuth.addresses,
|
|
4800
|
+
onRotateShares,
|
|
4801
|
+
onExportPortableBackup
|
|
4802
|
+
}
|
|
4803
|
+
), /* @__PURE__ */ React3.createElement("div", { style: {
|
|
4401
4804
|
padding: "14px 16px",
|
|
4402
4805
|
borderTop: "1px solid var(--dc-border)",
|
|
4403
4806
|
display: "flex",
|
|
@@ -4442,7 +4845,7 @@ function SocialWalletModal({
|
|
|
4442
4845
|
}
|
|
4443
4846
|
},
|
|
4444
4847
|
disconnecting ? "Signing out\u2026" : "Sign out"
|
|
4445
|
-
)))
|
|
4848
|
+
))))
|
|
4446
4849
|
);
|
|
4447
4850
|
return createPortal2(modal, document.body);
|
|
4448
4851
|
}
|
|
@@ -4460,7 +4863,8 @@ var DEFAULT_SOCIAL = {
|
|
|
4460
4863
|
addresses: null,
|
|
4461
4864
|
isNewUser: false,
|
|
4462
4865
|
isUnlocked: false,
|
|
4463
|
-
sessionExpiresAt: null
|
|
4866
|
+
sessionExpiresAt: null,
|
|
4867
|
+
needsReconnect: false
|
|
4464
4868
|
};
|
|
4465
4869
|
var DecaneContext = createContext(null);
|
|
4466
4870
|
function hasSocial(mode) {
|
|
@@ -4520,7 +4924,7 @@ function DecaneKit({ config, children }) {
|
|
|
4520
4924
|
sdk.on("disconnected", () => setSocialAuth(DEFAULT_SOCIAL));
|
|
4521
4925
|
const saved = sdk.getAddresses();
|
|
4522
4926
|
if (saved) {
|
|
4523
|
-
setSocialAuth((p) => ({ ...p, addresses: saved, isUnlocked: false }));
|
|
4927
|
+
setSocialAuth((p) => ({ ...p, addresses: saved, isUnlocked: false, needsReconnect: true }));
|
|
4524
4928
|
}
|
|
4525
4929
|
const params = new URLSearchParams(window.location.search);
|
|
4526
4930
|
if (params.has("decane_jwt") || params.has("decane_error")) {
|
|
@@ -4534,7 +4938,8 @@ function DecaneKit({ config, children }) {
|
|
|
4534
4938
|
addresses: result.addresses,
|
|
4535
4939
|
isNewUser: result.isNewUser,
|
|
4536
4940
|
isUnlocked: sdk?.isUnlocked() ?? false,
|
|
4537
|
-
sessionExpiresAt: sdk?.sessionExpiresAt() ?? null
|
|
4941
|
+
sessionExpiresAt: sdk?.sessionExpiresAt() ?? null,
|
|
4942
|
+
needsReconnect: false
|
|
4538
4943
|
});
|
|
4539
4944
|
}
|
|
4540
4945
|
const connectWithGoogle = useCallback3(async () => {
|
|
@@ -4561,7 +4966,8 @@ function DecaneKit({ config, children }) {
|
|
|
4561
4966
|
setSocialAuth((p) => ({
|
|
4562
4967
|
...p,
|
|
4563
4968
|
isUnlocked: sdk.isUnlocked(),
|
|
4564
|
-
sessionExpiresAt: sdk.sessionExpiresAt()
|
|
4969
|
+
sessionExpiresAt: sdk.sessionExpiresAt(),
|
|
4970
|
+
needsReconnect: false
|
|
4565
4971
|
}));
|
|
4566
4972
|
}, []);
|
|
4567
4973
|
const socialDisconnect = useCallback3(async () => {
|
|
@@ -4585,6 +4991,31 @@ function DecaneKit({ config, children }) {
|
|
|
4585
4991
|
if (!sdk) throw new Error("Social wallet not connected");
|
|
4586
4992
|
return sdk.signSolanaTransaction(tx);
|
|
4587
4993
|
}, []);
|
|
4994
|
+
const socialRotateShares = useCallback3(async (opts) => {
|
|
4995
|
+
const sdk = socialRef.current;
|
|
4996
|
+
if (!sdk) throw new Error("Social wallet not connected");
|
|
4997
|
+
return sdk.rotateShares(opts);
|
|
4998
|
+
}, []);
|
|
4999
|
+
const socialExportPortableBackup = useCallback3(async (opts) => {
|
|
5000
|
+
const sdk = socialRef.current;
|
|
5001
|
+
if (!sdk) throw new Error("Social wallet not connected");
|
|
5002
|
+
return sdk.exportPortableBackup(opts);
|
|
5003
|
+
}, []);
|
|
5004
|
+
const socialHasRecoveryShare = useCallback3(async () => {
|
|
5005
|
+
const sdk = socialRef.current;
|
|
5006
|
+
if (!sdk) throw new Error("Social wallet not connected");
|
|
5007
|
+
return sdk.hasRecoveryShare();
|
|
5008
|
+
}, []);
|
|
5009
|
+
const socialShouldPromptRecovery = useCallback3(async () => {
|
|
5010
|
+
const sdk = socialRef.current;
|
|
5011
|
+
if (!sdk) throw new Error("Social wallet not connected");
|
|
5012
|
+
return sdk.shouldPromptRecovery();
|
|
5013
|
+
}, []);
|
|
5014
|
+
const socialDismissRecoveryPrompt = useCallback3(async () => {
|
|
5015
|
+
const sdk = socialRef.current;
|
|
5016
|
+
if (!sdk) throw new Error("Social wallet not connected");
|
|
5017
|
+
return sdk.dismissRecoveryPrompt();
|
|
5018
|
+
}, []);
|
|
4588
5019
|
const handleWalletConnect = useCallback3(
|
|
4589
5020
|
(wallet, preference) => {
|
|
4590
5021
|
walletRef.current.connect(wallet, preference).catch(console.error);
|
|
@@ -4612,6 +5043,11 @@ function DecaneKit({ config, children }) {
|
|
|
4612
5043
|
socialSignMessage,
|
|
4613
5044
|
socialSendTransaction,
|
|
4614
5045
|
socialSignSolanaTransaction,
|
|
5046
|
+
socialRotateShares,
|
|
5047
|
+
socialExportPortableBackup,
|
|
5048
|
+
socialHasRecoveryShare,
|
|
5049
|
+
socialShouldPromptRecovery,
|
|
5050
|
+
socialDismissRecoveryPrompt,
|
|
4615
5051
|
socialModalOpen,
|
|
4616
5052
|
openSocialModal,
|
|
4617
5053
|
closeSocialModal
|
|
@@ -4642,7 +5078,15 @@ function DecaneKit({ config, children }) {
|
|
|
4642
5078
|
theme,
|
|
4643
5079
|
socialAuth,
|
|
4644
5080
|
onUnlock: socialUnlock,
|
|
4645
|
-
onDisconnect: socialDisconnect
|
|
5081
|
+
onDisconnect: socialDisconnect,
|
|
5082
|
+
onGoogleSignIn: connectWithGoogle,
|
|
5083
|
+
onEmailStart: connectWithEmail,
|
|
5084
|
+
onEmailVerify: verifyEmailCode2,
|
|
5085
|
+
authMethods: config?.social?.authMethods ?? ["google", "email"],
|
|
5086
|
+
onRotateShares: socialRotateShares,
|
|
5087
|
+
onExportPortableBackup: socialExportPortableBackup,
|
|
5088
|
+
rpcUrls: config?.social?.rpcUrls,
|
|
5089
|
+
chains: config?.social?.chains
|
|
4646
5090
|
}
|
|
4647
5091
|
)
|
|
4648
5092
|
);
|
|
@@ -4730,6 +5174,11 @@ function useSocialAuth() {
|
|
|
4730
5174
|
isNewUser: socialAuth.isNewUser,
|
|
4731
5175
|
addresses: socialAuth.addresses,
|
|
4732
5176
|
sessionExpiresAt: socialAuth.sessionExpiresAt,
|
|
5177
|
+
// True when addresses are known (from a previous visit) but there's no
|
|
5178
|
+
// live session — unlock() will always throw in this state (every
|
|
5179
|
+
// unlock requires a fresh JWT, and JWTs are never persisted). Route
|
|
5180
|
+
// this into a real reconnect (signInWithGoogle / sendEmailCode), not unlock().
|
|
5181
|
+
needsReconnect: socialAuth.needsReconnect,
|
|
4733
5182
|
googleLoading,
|
|
4734
5183
|
emailLoading,
|
|
4735
5184
|
error,
|
|
@@ -4749,6 +5198,11 @@ function useSocialWallet() {
|
|
|
4749
5198
|
socialSignMessage,
|
|
4750
5199
|
socialSendTransaction,
|
|
4751
5200
|
socialSignSolanaTransaction,
|
|
5201
|
+
socialRotateShares,
|
|
5202
|
+
socialExportPortableBackup,
|
|
5203
|
+
socialHasRecoveryShare,
|
|
5204
|
+
socialShouldPromptRecovery,
|
|
5205
|
+
socialDismissRecoveryPrompt,
|
|
4752
5206
|
openSocialModal,
|
|
4753
5207
|
closeSocialModal,
|
|
4754
5208
|
socialModalOpen
|
|
@@ -4798,6 +5252,26 @@ function useSocialWallet() {
|
|
|
4798
5252
|
throw e;
|
|
4799
5253
|
}
|
|
4800
5254
|
}, [socialDisconnect]);
|
|
5255
|
+
const rotateShares = useCallback4(
|
|
5256
|
+
(opts) => run(() => socialRotateShares(opts)),
|
|
5257
|
+
[socialRotateShares]
|
|
5258
|
+
);
|
|
5259
|
+
const exportPortableBackup = useCallback4(
|
|
5260
|
+
(opts) => run(() => socialExportPortableBackup(opts)),
|
|
5261
|
+
[socialExportPortableBackup]
|
|
5262
|
+
);
|
|
5263
|
+
const hasRecoveryShare = useCallback4(
|
|
5264
|
+
() => run(() => socialHasRecoveryShare()),
|
|
5265
|
+
[socialHasRecoveryShare]
|
|
5266
|
+
);
|
|
5267
|
+
const shouldPromptRecovery = useCallback4(
|
|
5268
|
+
() => run(() => socialShouldPromptRecovery()),
|
|
5269
|
+
[socialShouldPromptRecovery]
|
|
5270
|
+
);
|
|
5271
|
+
const dismissRecoveryPrompt = useCallback4(
|
|
5272
|
+
() => run(() => socialDismissRecoveryPrompt()),
|
|
5273
|
+
[socialDismissRecoveryPrompt]
|
|
5274
|
+
);
|
|
4801
5275
|
return {
|
|
4802
5276
|
// State
|
|
4803
5277
|
isConnected: !!socialAuth.addresses,
|
|
@@ -4805,6 +5279,11 @@ function useSocialWallet() {
|
|
|
4805
5279
|
isNewUser: socialAuth.isNewUser,
|
|
4806
5280
|
addresses: socialAuth.addresses,
|
|
4807
5281
|
sessionExpiresAt: socialAuth.sessionExpiresAt,
|
|
5282
|
+
// True when addresses are known (from a previous visit) but there's no
|
|
5283
|
+
// live session — unlock() will always throw in this state (every
|
|
5284
|
+
// unlock requires a fresh JWT, and JWTs are never persisted). Route
|
|
5285
|
+
// this into a real reconnect (signInWithGoogle / sendEmailCode), not unlock().
|
|
5286
|
+
needsReconnect: socialAuth.needsReconnect,
|
|
4808
5287
|
// Modal
|
|
4809
5288
|
modalOpen: socialModalOpen,
|
|
4810
5289
|
openModal: openSocialModal,
|
|
@@ -4816,6 +5295,12 @@ function useSocialWallet() {
|
|
|
4816
5295
|
signMessage,
|
|
4817
5296
|
sendTransaction,
|
|
4818
5297
|
signSolanaTransaction: signSolanaTransaction2,
|
|
5298
|
+
// Recovery (2-of-3 Shamir share set)
|
|
5299
|
+
rotateShares,
|
|
5300
|
+
exportPortableBackup,
|
|
5301
|
+
hasRecoveryShare,
|
|
5302
|
+
shouldPromptRecovery,
|
|
5303
|
+
dismissRecoveryPrompt,
|
|
4819
5304
|
loading,
|
|
4820
5305
|
error,
|
|
4821
5306
|
clearError: () => setError(null)
|
|
@@ -5061,6 +5546,7 @@ export {
|
|
|
5061
5546
|
NewDeviceError,
|
|
5062
5547
|
OriginNotAllowedError,
|
|
5063
5548
|
PromptPinNotConfiguredError,
|
|
5549
|
+
ReconnectRequiredError,
|
|
5064
5550
|
RecoveryFailedError,
|
|
5065
5551
|
RecoveryWalletMismatchError,
|
|
5066
5552
|
SessionExpiredError,
|