decane-connect-kit 2.0.0 → 2.0.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/dist/index.cjs +521 -37
- 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 +520 -37
- 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();
|
|
@@ -4196,6 +4283,15 @@ function countdown(expiresAt) {
|
|
|
4196
4283
|
const secs = Math.max(0, Math.round((expiresAt - Date.now()) / 1e3));
|
|
4197
4284
|
return `${Math.floor(secs / 60)}m ${String(secs % 60).padStart(2, "0")}s`;
|
|
4198
4285
|
}
|
|
4286
|
+
function downloadJson(data, filename) {
|
|
4287
|
+
const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" });
|
|
4288
|
+
const url = URL.createObjectURL(blob);
|
|
4289
|
+
const a = document.createElement("a");
|
|
4290
|
+
a.href = url;
|
|
4291
|
+
a.download = filename;
|
|
4292
|
+
a.click();
|
|
4293
|
+
URL.revokeObjectURL(url);
|
|
4294
|
+
}
|
|
4199
4295
|
function CopyButton({ value }) {
|
|
4200
4296
|
const [copied, setCopied] = useState2(false);
|
|
4201
4297
|
async function copy() {
|
|
@@ -4223,7 +4319,54 @@ function CopyButton({ value }) {
|
|
|
4223
4319
|
copied ? "\u2713" : "copy"
|
|
4224
4320
|
);
|
|
4225
4321
|
}
|
|
4226
|
-
function
|
|
4322
|
+
function useBalance(kind, address, chain, rpcUrls) {
|
|
4323
|
+
const [state, setState] = useState2({ loading: false, value: null, symbol: kind === "evm" ? "ETH" : "SOL" });
|
|
4324
|
+
useEffect2(() => {
|
|
4325
|
+
if (!address || !chain) return;
|
|
4326
|
+
let cancelled = false;
|
|
4327
|
+
setState((s) => ({ ...s, loading: true }));
|
|
4328
|
+
(async () => {
|
|
4329
|
+
try {
|
|
4330
|
+
if (kind === "evm") {
|
|
4331
|
+
const rpcUrl = rpcUrls?.[chain] ?? DEFAULT_RPCS[chain];
|
|
4332
|
+
if (!rpcUrl) {
|
|
4333
|
+
if (!cancelled) setState({ loading: false, value: null, symbol: "ETH" });
|
|
4334
|
+
return;
|
|
4335
|
+
}
|
|
4336
|
+
const { JsonRpcProvider, formatEther } = await import("ethers");
|
|
4337
|
+
const wei = await new JsonRpcProvider(rpcUrl).getBalance(address);
|
|
4338
|
+
if (!cancelled) setState({ loading: false, value: formatEther(wei), symbol: "ETH" });
|
|
4339
|
+
} else {
|
|
4340
|
+
const cluster = chain.slice("solana:".length);
|
|
4341
|
+
const endpoint = rpcUrls?.[chain] ?? (cluster === "devnet" ? "https://api.devnet.solana.com" : "https://api.mainnet-beta.solana.com");
|
|
4342
|
+
const { Connection, PublicKey, LAMPORTS_PER_SOL } = await import("@solana/web3.js");
|
|
4343
|
+
const lamports = await new Connection(endpoint).getBalance(new PublicKey(address));
|
|
4344
|
+
if (!cancelled) setState({ loading: false, value: String(lamports / LAMPORTS_PER_SOL), symbol: "SOL" });
|
|
4345
|
+
}
|
|
4346
|
+
} catch {
|
|
4347
|
+
if (!cancelled) setState({ loading: false, value: null, symbol: kind === "evm" ? "ETH" : "SOL" });
|
|
4348
|
+
}
|
|
4349
|
+
})();
|
|
4350
|
+
return () => {
|
|
4351
|
+
cancelled = true;
|
|
4352
|
+
};
|
|
4353
|
+
}, [kind, address, chain, rpcUrls]);
|
|
4354
|
+
return state;
|
|
4355
|
+
}
|
|
4356
|
+
function formatBalance(value) {
|
|
4357
|
+
if (value == null) return "\u2014";
|
|
4358
|
+
const n = Number(value);
|
|
4359
|
+
if (!Number.isFinite(n)) return value;
|
|
4360
|
+
return n === 0 ? "0" : n < 1e-4 ? "<0.0001" : n.toFixed(4).replace(/\.?0+$/, "");
|
|
4361
|
+
}
|
|
4362
|
+
function AddressRow({
|
|
4363
|
+
label,
|
|
4364
|
+
address,
|
|
4365
|
+
kind,
|
|
4366
|
+
chain,
|
|
4367
|
+
rpcUrls
|
|
4368
|
+
}) {
|
|
4369
|
+
const balance = useBalance(kind, address, chain, rpcUrls);
|
|
4227
4370
|
const short = `${address.slice(0, 8)}\u2026${address.slice(-6)}`;
|
|
4228
4371
|
return /* @__PURE__ */ React3.createElement("div", { style: {
|
|
4229
4372
|
background: "var(--dc-surface)",
|
|
@@ -4231,7 +4374,233 @@ function AddressRow({ label, address }) {
|
|
|
4231
4374
|
borderRadius: 10,
|
|
4232
4375
|
padding: "10px 14px",
|
|
4233
4376
|
marginBottom: 8
|
|
4234
|
-
} }, /* @__PURE__ */ React3.createElement("div", { style: { fontSize: 10, fontWeight: 700, color: "var(--dc-text-muted)", letterSpacing: "0.06em", textTransform: "uppercase",
|
|
4377
|
+
} }, /* @__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 })));
|
|
4378
|
+
}
|
|
4379
|
+
function SignInView({
|
|
4380
|
+
onGoogleSignIn,
|
|
4381
|
+
onEmailStart,
|
|
4382
|
+
onEmailVerify,
|
|
4383
|
+
authMethods,
|
|
4384
|
+
resumeHint
|
|
4385
|
+
}) {
|
|
4386
|
+
const [googleLoading, setGoogleLoading] = useState2(false);
|
|
4387
|
+
const [email, setEmail] = useState2("");
|
|
4388
|
+
const [code, setCode] = useState2("");
|
|
4389
|
+
const [emailSent, setEmailSent] = useState2(false);
|
|
4390
|
+
const [emailLoading, setEmailLoading] = useState2(false);
|
|
4391
|
+
const [error, setError] = useState2("");
|
|
4392
|
+
async function handleGoogle() {
|
|
4393
|
+
setError("");
|
|
4394
|
+
setGoogleLoading(true);
|
|
4395
|
+
try {
|
|
4396
|
+
await onGoogleSignIn();
|
|
4397
|
+
} catch (e) {
|
|
4398
|
+
setError(e.message);
|
|
4399
|
+
setGoogleLoading(false);
|
|
4400
|
+
}
|
|
4401
|
+
}
|
|
4402
|
+
async function handleEmailStart(e) {
|
|
4403
|
+
e.preventDefault();
|
|
4404
|
+
setError("");
|
|
4405
|
+
setEmailLoading(true);
|
|
4406
|
+
try {
|
|
4407
|
+
await onEmailStart(email);
|
|
4408
|
+
setEmailSent(true);
|
|
4409
|
+
} catch (e2) {
|
|
4410
|
+
setError(e2.message);
|
|
4411
|
+
} finally {
|
|
4412
|
+
setEmailLoading(false);
|
|
4413
|
+
}
|
|
4414
|
+
}
|
|
4415
|
+
async function handleEmailVerify(e) {
|
|
4416
|
+
e.preventDefault();
|
|
4417
|
+
setError("");
|
|
4418
|
+
setEmailLoading(true);
|
|
4419
|
+
try {
|
|
4420
|
+
await onEmailVerify(email, code);
|
|
4421
|
+
} catch (e2) {
|
|
4422
|
+
setError(e2.message);
|
|
4423
|
+
} finally {
|
|
4424
|
+
setEmailLoading(false);
|
|
4425
|
+
}
|
|
4426
|
+
}
|
|
4427
|
+
const inputStyle = {
|
|
4428
|
+
width: "100%",
|
|
4429
|
+
background: "var(--dc-surface)",
|
|
4430
|
+
border: "1px solid var(--dc-border)",
|
|
4431
|
+
borderRadius: 8,
|
|
4432
|
+
color: "var(--dc-text)",
|
|
4433
|
+
padding: "9px 12px",
|
|
4434
|
+
fontSize: 13,
|
|
4435
|
+
outline: "none",
|
|
4436
|
+
fontFamily: "inherit",
|
|
4437
|
+
boxSizing: "border-box"
|
|
4438
|
+
};
|
|
4439
|
+
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(
|
|
4440
|
+
"button",
|
|
4441
|
+
{
|
|
4442
|
+
onClick: handleGoogle,
|
|
4443
|
+
disabled: googleLoading,
|
|
4444
|
+
style: {
|
|
4445
|
+
width: "100%",
|
|
4446
|
+
padding: "10px 0",
|
|
4447
|
+
marginBottom: 10,
|
|
4448
|
+
background: "var(--dc-accent)",
|
|
4449
|
+
color: "var(--dc-accent-text)",
|
|
4450
|
+
border: "none",
|
|
4451
|
+
borderRadius: 10,
|
|
4452
|
+
fontSize: 13,
|
|
4453
|
+
fontWeight: 700,
|
|
4454
|
+
cursor: googleLoading ? "not-allowed" : "pointer",
|
|
4455
|
+
opacity: googleLoading ? 0.7 : 1,
|
|
4456
|
+
fontFamily: "inherit"
|
|
4457
|
+
}
|
|
4458
|
+
},
|
|
4459
|
+
googleLoading ? "Redirecting\u2026" : "Continue with Google"
|
|
4460
|
+
), 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(
|
|
4461
|
+
"input",
|
|
4462
|
+
{
|
|
4463
|
+
type: "email",
|
|
4464
|
+
required: true,
|
|
4465
|
+
placeholder: "your@email.com",
|
|
4466
|
+
value: email,
|
|
4467
|
+
onChange: (e) => setEmail(e.target.value),
|
|
4468
|
+
style: { ...inputStyle, flex: 1 }
|
|
4469
|
+
}
|
|
4470
|
+
), /* @__PURE__ */ React3.createElement("button", { type: "submit", disabled: emailLoading, style: {
|
|
4471
|
+
padding: "0 16px",
|
|
4472
|
+
background: "var(--dc-surface)",
|
|
4473
|
+
border: "1px solid var(--dc-border)",
|
|
4474
|
+
borderRadius: 8,
|
|
4475
|
+
color: "var(--dc-text)",
|
|
4476
|
+
fontSize: 13,
|
|
4477
|
+
cursor: "pointer",
|
|
4478
|
+
fontFamily: "inherit"
|
|
4479
|
+
} }, emailLoading ? "\u2026" : "Send code")) : /* @__PURE__ */ React3.createElement("form", { onSubmit: handleEmailVerify, style: { display: "flex", gap: 8 } }, /* @__PURE__ */ React3.createElement(
|
|
4480
|
+
"input",
|
|
4481
|
+
{
|
|
4482
|
+
placeholder: "6-digit code",
|
|
4483
|
+
value: code,
|
|
4484
|
+
maxLength: 6,
|
|
4485
|
+
autoFocus: true,
|
|
4486
|
+
onChange: (e) => setCode(e.target.value.replace(/\D/g, "")),
|
|
4487
|
+
style: { ...inputStyle, flex: 1, letterSpacing: 4, textAlign: "center" }
|
|
4488
|
+
}
|
|
4489
|
+
), /* @__PURE__ */ React3.createElement("button", { type: "submit", disabled: emailLoading, style: {
|
|
4490
|
+
padding: "0 16px",
|
|
4491
|
+
background: "var(--dc-accent)",
|
|
4492
|
+
color: "var(--dc-accent-text)",
|
|
4493
|
+
border: "none",
|
|
4494
|
+
borderRadius: 8,
|
|
4495
|
+
fontSize: 13,
|
|
4496
|
+
fontWeight: 700,
|
|
4497
|
+
cursor: "pointer",
|
|
4498
|
+
fontFamily: "inherit"
|
|
4499
|
+
} }, emailLoading ? "\u2026" : "Verify"))), error && /* @__PURE__ */ React3.createElement("div", { style: { marginTop: 10, fontSize: 12, color: "var(--dc-danger, #ff5533)" } }, error));
|
|
4500
|
+
}
|
|
4501
|
+
function RecoverySection({
|
|
4502
|
+
addresses,
|
|
4503
|
+
onRotateShares,
|
|
4504
|
+
onExportPortableBackup
|
|
4505
|
+
}) {
|
|
4506
|
+
const [expanded, setExpanded] = useState2(false);
|
|
4507
|
+
const [mode, setMode] = useState2(null);
|
|
4508
|
+
const [password, setPassword] = useState2("");
|
|
4509
|
+
const [passwordHint, setPasswordHint] = useState2("");
|
|
4510
|
+
const [confirmed, setConfirmed] = useState2(false);
|
|
4511
|
+
const [busy, setBusy] = useState2(false);
|
|
4512
|
+
const [result, setResult] = useState2(null);
|
|
4513
|
+
const [error, setError] = useState2("");
|
|
4514
|
+
function reset() {
|
|
4515
|
+
setMode(null);
|
|
4516
|
+
setPassword("");
|
|
4517
|
+
setPasswordHint("");
|
|
4518
|
+
setConfirmed(false);
|
|
4519
|
+
setError("");
|
|
4520
|
+
}
|
|
4521
|
+
async function handleGenerate() {
|
|
4522
|
+
if (!addresses?.evm || !mode) return;
|
|
4523
|
+
if (password.length < 8) {
|
|
4524
|
+
setError("Password must be at least 8 characters.");
|
|
4525
|
+
return;
|
|
4526
|
+
}
|
|
4527
|
+
if (mode === "portable" && !confirmed) {
|
|
4528
|
+
setError("Confirm you understand the risk first.");
|
|
4529
|
+
return;
|
|
4530
|
+
}
|
|
4531
|
+
setBusy(true);
|
|
4532
|
+
setError("");
|
|
4533
|
+
try {
|
|
4534
|
+
if (mode === "share") {
|
|
4535
|
+
const file = await onRotateShares({ password, passwordHint: passwordHint || void 0 });
|
|
4536
|
+
downloadJson(file, `decane-recovery-${addresses.evm.slice(0, 8)}.json`);
|
|
4537
|
+
setResult({ kind: "share", version: file.shareSetVersion });
|
|
4538
|
+
} else {
|
|
4539
|
+
const file = await onExportPortableBackup({ password, passwordHint: passwordHint || void 0 });
|
|
4540
|
+
downloadJson(file, `decane-PORTABLE-BACKUP-${addresses.evm.slice(0, 8)}.json`);
|
|
4541
|
+
setResult({ kind: "portable", version: file.shareSetVersion });
|
|
4542
|
+
}
|
|
4543
|
+
reset();
|
|
4544
|
+
} catch (e) {
|
|
4545
|
+
setError(e.message);
|
|
4546
|
+
} finally {
|
|
4547
|
+
setBusy(false);
|
|
4548
|
+
}
|
|
4549
|
+
}
|
|
4550
|
+
const inputStyle = {
|
|
4551
|
+
width: "100%",
|
|
4552
|
+
background: "var(--dc-bg)",
|
|
4553
|
+
border: "1px solid var(--dc-border)",
|
|
4554
|
+
borderRadius: 8,
|
|
4555
|
+
color: "var(--dc-text)",
|
|
4556
|
+
padding: "8px 10px",
|
|
4557
|
+
fontSize: 12,
|
|
4558
|
+
outline: "none",
|
|
4559
|
+
fontFamily: "inherit",
|
|
4560
|
+
boxSizing: "border-box"
|
|
4561
|
+
};
|
|
4562
|
+
const btnStyle = {
|
|
4563
|
+
padding: "8px 12px",
|
|
4564
|
+
borderRadius: 8,
|
|
4565
|
+
fontSize: 12,
|
|
4566
|
+
fontWeight: 700,
|
|
4567
|
+
cursor: "pointer",
|
|
4568
|
+
fontFamily: "inherit",
|
|
4569
|
+
border: "none"
|
|
4570
|
+
};
|
|
4571
|
+
return /* @__PURE__ */ React3.createElement("div", { style: { margin: "0 16px 14px", border: "1px solid var(--dc-border)", borderRadius: 10, overflow: "hidden" } }, /* @__PURE__ */ React3.createElement(
|
|
4572
|
+
"button",
|
|
4573
|
+
{
|
|
4574
|
+
onClick: () => setExpanded((v) => !v),
|
|
4575
|
+
style: {
|
|
4576
|
+
width: "100%",
|
|
4577
|
+
padding: "10px 14px",
|
|
4578
|
+
background: "var(--dc-surface)",
|
|
4579
|
+
border: "none",
|
|
4580
|
+
display: "flex",
|
|
4581
|
+
alignItems: "center",
|
|
4582
|
+
justifyContent: "space-between",
|
|
4583
|
+
cursor: "pointer",
|
|
4584
|
+
fontFamily: "inherit"
|
|
4585
|
+
}
|
|
4586
|
+
},
|
|
4587
|
+
/* @__PURE__ */ React3.createElement("span", { style: { fontSize: 12, fontWeight: 700, color: "var(--dc-text)" } }, "Recovery"),
|
|
4588
|
+
/* @__PURE__ */ React3.createElement("span", { style: { fontSize: 11, color: "var(--dc-text-muted)" } }, expanded ? "\u25B2" : "\u25BC")
|
|
4589
|
+
), 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: () => {
|
|
4590
|
+
reset();
|
|
4591
|
+
setMode("share");
|
|
4592
|
+
}, style: { ...btnStyle, background: "var(--dc-accent)", color: "var(--dc-accent-text)" } }, "Generate recovery-share file"), /* @__PURE__ */ React3.createElement("button", { onClick: () => {
|
|
4593
|
+
reset();
|
|
4594
|
+
setMode("portable");
|
|
4595
|
+
}, style: { ...btnStyle, background: "transparent", border: "1px solid var(--dc-border)", color: "var(--dc-text)" } }, "Generate portable backup (Decane-is-down)")), 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: {
|
|
4596
|
+
background: "rgba(255,85,51,0.08)",
|
|
4597
|
+
border: "1px solid rgba(255,85,51,0.3)",
|
|
4598
|
+
borderRadius: 8,
|
|
4599
|
+
padding: "8px 10px",
|
|
4600
|
+
fontSize: 11,
|
|
4601
|
+
color: "#ff9977",
|
|
4602
|
+
lineHeight: 1.5
|
|
4603
|
+
} }, /* @__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" : "Portable backup", " downloaded \u2014 share set is now version ", result.version, ". Any previous recovery file no longer works.")));
|
|
4235
4604
|
}
|
|
4236
4605
|
function SocialWalletModal({
|
|
4237
4606
|
open,
|
|
@@ -4239,7 +4608,15 @@ function SocialWalletModal({
|
|
|
4239
4608
|
theme = "auto",
|
|
4240
4609
|
socialAuth,
|
|
4241
4610
|
onUnlock,
|
|
4242
|
-
onDisconnect
|
|
4611
|
+
onDisconnect,
|
|
4612
|
+
onGoogleSignIn,
|
|
4613
|
+
onEmailStart,
|
|
4614
|
+
onEmailVerify,
|
|
4615
|
+
authMethods = ["google", "email"],
|
|
4616
|
+
onRotateShares,
|
|
4617
|
+
onExportPortableBackup,
|
|
4618
|
+
rpcUrls,
|
|
4619
|
+
chains
|
|
4243
4620
|
}) {
|
|
4244
4621
|
const [timer, setTimer] = useState2("");
|
|
4245
4622
|
const [unlocking, setUnlocking] = useState2(false);
|
|
@@ -4288,6 +4665,9 @@ function SocialWalletModal({
|
|
|
4288
4665
|
setDisconnecting(false);
|
|
4289
4666
|
}
|
|
4290
4667
|
}
|
|
4668
|
+
const evmChain = chains?.find((c) => c.startsWith("evm:")) ?? "evm:11155111";
|
|
4669
|
+
const solChain = chains?.find((c) => c.startsWith("solana:")) ?? "solana:mainnet";
|
|
4670
|
+
const showSignIn = !socialAuth.addresses || socialAuth.needsReconnect;
|
|
4291
4671
|
const modal = /* @__PURE__ */ React3.createElement(
|
|
4292
4672
|
"div",
|
|
4293
4673
|
{
|
|
@@ -4325,10 +4705,14 @@ function SocialWalletModal({
|
|
|
4325
4705
|
borderRadius: 20,
|
|
4326
4706
|
width: "100%",
|
|
4327
4707
|
maxWidth: 380,
|
|
4328
|
-
|
|
4329
|
-
|
|
4708
|
+
maxHeight: "90vh",
|
|
4709
|
+
overflowY: "auto",
|
|
4710
|
+
boxShadow: "0 24px 64px rgba(0,0,0,0.6), 0 0 0 1px rgba(255,255,255,0.04)"
|
|
4330
4711
|
} }, /* @__PURE__ */ React3.createElement("div", { style: {
|
|
4331
4712
|
height: 2,
|
|
4713
|
+
position: "sticky",
|
|
4714
|
+
top: 0,
|
|
4715
|
+
zIndex: 1,
|
|
4332
4716
|
background: "linear-gradient(90deg, transparent, var(--dc-accent) 30%, var(--dc-accent) 70%, transparent)"
|
|
4333
4717
|
} }), /* @__PURE__ */ React3.createElement("div", { style: { padding: "18px 20px 16px", display: "flex", alignItems: "center", gap: 10 } }, /* @__PURE__ */ React3.createElement("div", { style: {
|
|
4334
4718
|
width: 34,
|
|
@@ -4357,7 +4741,7 @@ function SocialWalletModal({
|
|
|
4357
4741
|
strokeLinecap: "round",
|
|
4358
4742
|
strokeLinejoin: "round"
|
|
4359
4743
|
}
|
|
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
|
|
4744
|
+
))), /* @__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
4745
|
"button",
|
|
4362
4746
|
{
|
|
4363
4747
|
onClick: onClose,
|
|
@@ -4377,7 +4761,16 @@ function SocialWalletModal({
|
|
|
4377
4761
|
}
|
|
4378
4762
|
},
|
|
4379
4763
|
/* @__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(
|
|
4764
|
+
)), showSignIn ? /* @__PURE__ */ React3.createElement(
|
|
4765
|
+
SignInView,
|
|
4766
|
+
{
|
|
4767
|
+
onGoogleSignIn,
|
|
4768
|
+
onEmailStart,
|
|
4769
|
+
onEmailVerify,
|
|
4770
|
+
authMethods,
|
|
4771
|
+
resumeHint: socialAuth.needsReconnect ? "your wallet" : void 0
|
|
4772
|
+
}
|
|
4773
|
+
) : /* @__PURE__ */ React3.createElement(React3.Fragment, null, /* @__PURE__ */ React3.createElement("div", { style: {
|
|
4381
4774
|
margin: "0 16px 14px",
|
|
4382
4775
|
padding: "8px 14px",
|
|
4383
4776
|
background: socialAuth.isUnlocked ? "rgba(18,183,106,0.08)" : "rgba(245,200,0,0.08)",
|
|
@@ -4397,7 +4790,14 @@ function SocialWalletModal({
|
|
|
4397
4790
|
fontSize: 12,
|
|
4398
4791
|
fontWeight: 700,
|
|
4399
4792
|
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
|
|
4793
|
+
} }, 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(
|
|
4794
|
+
RecoverySection,
|
|
4795
|
+
{
|
|
4796
|
+
addresses: socialAuth.addresses,
|
|
4797
|
+
onRotateShares,
|
|
4798
|
+
onExportPortableBackup
|
|
4799
|
+
}
|
|
4800
|
+
), /* @__PURE__ */ React3.createElement("div", { style: {
|
|
4401
4801
|
padding: "14px 16px",
|
|
4402
4802
|
borderTop: "1px solid var(--dc-border)",
|
|
4403
4803
|
display: "flex",
|
|
@@ -4442,7 +4842,7 @@ function SocialWalletModal({
|
|
|
4442
4842
|
}
|
|
4443
4843
|
},
|
|
4444
4844
|
disconnecting ? "Signing out\u2026" : "Sign out"
|
|
4445
|
-
)))
|
|
4845
|
+
))))
|
|
4446
4846
|
);
|
|
4447
4847
|
return createPortal2(modal, document.body);
|
|
4448
4848
|
}
|
|
@@ -4460,7 +4860,8 @@ var DEFAULT_SOCIAL = {
|
|
|
4460
4860
|
addresses: null,
|
|
4461
4861
|
isNewUser: false,
|
|
4462
4862
|
isUnlocked: false,
|
|
4463
|
-
sessionExpiresAt: null
|
|
4863
|
+
sessionExpiresAt: null,
|
|
4864
|
+
needsReconnect: false
|
|
4464
4865
|
};
|
|
4465
4866
|
var DecaneContext = createContext(null);
|
|
4466
4867
|
function hasSocial(mode) {
|
|
@@ -4520,7 +4921,7 @@ function DecaneKit({ config, children }) {
|
|
|
4520
4921
|
sdk.on("disconnected", () => setSocialAuth(DEFAULT_SOCIAL));
|
|
4521
4922
|
const saved = sdk.getAddresses();
|
|
4522
4923
|
if (saved) {
|
|
4523
|
-
setSocialAuth((p) => ({ ...p, addresses: saved, isUnlocked: false }));
|
|
4924
|
+
setSocialAuth((p) => ({ ...p, addresses: saved, isUnlocked: false, needsReconnect: true }));
|
|
4524
4925
|
}
|
|
4525
4926
|
const params = new URLSearchParams(window.location.search);
|
|
4526
4927
|
if (params.has("decane_jwt") || params.has("decane_error")) {
|
|
@@ -4534,7 +4935,8 @@ function DecaneKit({ config, children }) {
|
|
|
4534
4935
|
addresses: result.addresses,
|
|
4535
4936
|
isNewUser: result.isNewUser,
|
|
4536
4937
|
isUnlocked: sdk?.isUnlocked() ?? false,
|
|
4537
|
-
sessionExpiresAt: sdk?.sessionExpiresAt() ?? null
|
|
4938
|
+
sessionExpiresAt: sdk?.sessionExpiresAt() ?? null,
|
|
4939
|
+
needsReconnect: false
|
|
4538
4940
|
});
|
|
4539
4941
|
}
|
|
4540
4942
|
const connectWithGoogle = useCallback3(async () => {
|
|
@@ -4561,7 +4963,8 @@ function DecaneKit({ config, children }) {
|
|
|
4561
4963
|
setSocialAuth((p) => ({
|
|
4562
4964
|
...p,
|
|
4563
4965
|
isUnlocked: sdk.isUnlocked(),
|
|
4564
|
-
sessionExpiresAt: sdk.sessionExpiresAt()
|
|
4966
|
+
sessionExpiresAt: sdk.sessionExpiresAt(),
|
|
4967
|
+
needsReconnect: false
|
|
4565
4968
|
}));
|
|
4566
4969
|
}, []);
|
|
4567
4970
|
const socialDisconnect = useCallback3(async () => {
|
|
@@ -4585,6 +4988,31 @@ function DecaneKit({ config, children }) {
|
|
|
4585
4988
|
if (!sdk) throw new Error("Social wallet not connected");
|
|
4586
4989
|
return sdk.signSolanaTransaction(tx);
|
|
4587
4990
|
}, []);
|
|
4991
|
+
const socialRotateShares = useCallback3(async (opts) => {
|
|
4992
|
+
const sdk = socialRef.current;
|
|
4993
|
+
if (!sdk) throw new Error("Social wallet not connected");
|
|
4994
|
+
return sdk.rotateShares(opts);
|
|
4995
|
+
}, []);
|
|
4996
|
+
const socialExportPortableBackup = useCallback3(async (opts) => {
|
|
4997
|
+
const sdk = socialRef.current;
|
|
4998
|
+
if (!sdk) throw new Error("Social wallet not connected");
|
|
4999
|
+
return sdk.exportPortableBackup(opts);
|
|
5000
|
+
}, []);
|
|
5001
|
+
const socialHasRecoveryShare = useCallback3(async () => {
|
|
5002
|
+
const sdk = socialRef.current;
|
|
5003
|
+
if (!sdk) throw new Error("Social wallet not connected");
|
|
5004
|
+
return sdk.hasRecoveryShare();
|
|
5005
|
+
}, []);
|
|
5006
|
+
const socialShouldPromptRecovery = useCallback3(async () => {
|
|
5007
|
+
const sdk = socialRef.current;
|
|
5008
|
+
if (!sdk) throw new Error("Social wallet not connected");
|
|
5009
|
+
return sdk.shouldPromptRecovery();
|
|
5010
|
+
}, []);
|
|
5011
|
+
const socialDismissRecoveryPrompt = useCallback3(async () => {
|
|
5012
|
+
const sdk = socialRef.current;
|
|
5013
|
+
if (!sdk) throw new Error("Social wallet not connected");
|
|
5014
|
+
return sdk.dismissRecoveryPrompt();
|
|
5015
|
+
}, []);
|
|
4588
5016
|
const handleWalletConnect = useCallback3(
|
|
4589
5017
|
(wallet, preference) => {
|
|
4590
5018
|
walletRef.current.connect(wallet, preference).catch(console.error);
|
|
@@ -4612,6 +5040,11 @@ function DecaneKit({ config, children }) {
|
|
|
4612
5040
|
socialSignMessage,
|
|
4613
5041
|
socialSendTransaction,
|
|
4614
5042
|
socialSignSolanaTransaction,
|
|
5043
|
+
socialRotateShares,
|
|
5044
|
+
socialExportPortableBackup,
|
|
5045
|
+
socialHasRecoveryShare,
|
|
5046
|
+
socialShouldPromptRecovery,
|
|
5047
|
+
socialDismissRecoveryPrompt,
|
|
4615
5048
|
socialModalOpen,
|
|
4616
5049
|
openSocialModal,
|
|
4617
5050
|
closeSocialModal
|
|
@@ -4642,7 +5075,15 @@ function DecaneKit({ config, children }) {
|
|
|
4642
5075
|
theme,
|
|
4643
5076
|
socialAuth,
|
|
4644
5077
|
onUnlock: socialUnlock,
|
|
4645
|
-
onDisconnect: socialDisconnect
|
|
5078
|
+
onDisconnect: socialDisconnect,
|
|
5079
|
+
onGoogleSignIn: connectWithGoogle,
|
|
5080
|
+
onEmailStart: connectWithEmail,
|
|
5081
|
+
onEmailVerify: verifyEmailCode2,
|
|
5082
|
+
authMethods: config?.social?.authMethods ?? ["google", "email"],
|
|
5083
|
+
onRotateShares: socialRotateShares,
|
|
5084
|
+
onExportPortableBackup: socialExportPortableBackup,
|
|
5085
|
+
rpcUrls: config?.social?.rpcUrls,
|
|
5086
|
+
chains: config?.social?.chains
|
|
4646
5087
|
}
|
|
4647
5088
|
)
|
|
4648
5089
|
);
|
|
@@ -4730,6 +5171,11 @@ function useSocialAuth() {
|
|
|
4730
5171
|
isNewUser: socialAuth.isNewUser,
|
|
4731
5172
|
addresses: socialAuth.addresses,
|
|
4732
5173
|
sessionExpiresAt: socialAuth.sessionExpiresAt,
|
|
5174
|
+
// True when addresses are known (from a previous visit) but there's no
|
|
5175
|
+
// live session — unlock() will always throw in this state (every
|
|
5176
|
+
// unlock requires a fresh JWT, and JWTs are never persisted). Route
|
|
5177
|
+
// this into a real reconnect (signInWithGoogle / sendEmailCode), not unlock().
|
|
5178
|
+
needsReconnect: socialAuth.needsReconnect,
|
|
4733
5179
|
googleLoading,
|
|
4734
5180
|
emailLoading,
|
|
4735
5181
|
error,
|
|
@@ -4749,6 +5195,11 @@ function useSocialWallet() {
|
|
|
4749
5195
|
socialSignMessage,
|
|
4750
5196
|
socialSendTransaction,
|
|
4751
5197
|
socialSignSolanaTransaction,
|
|
5198
|
+
socialRotateShares,
|
|
5199
|
+
socialExportPortableBackup,
|
|
5200
|
+
socialHasRecoveryShare,
|
|
5201
|
+
socialShouldPromptRecovery,
|
|
5202
|
+
socialDismissRecoveryPrompt,
|
|
4752
5203
|
openSocialModal,
|
|
4753
5204
|
closeSocialModal,
|
|
4754
5205
|
socialModalOpen
|
|
@@ -4798,6 +5249,26 @@ function useSocialWallet() {
|
|
|
4798
5249
|
throw e;
|
|
4799
5250
|
}
|
|
4800
5251
|
}, [socialDisconnect]);
|
|
5252
|
+
const rotateShares = useCallback4(
|
|
5253
|
+
(opts) => run(() => socialRotateShares(opts)),
|
|
5254
|
+
[socialRotateShares]
|
|
5255
|
+
);
|
|
5256
|
+
const exportPortableBackup = useCallback4(
|
|
5257
|
+
(opts) => run(() => socialExportPortableBackup(opts)),
|
|
5258
|
+
[socialExportPortableBackup]
|
|
5259
|
+
);
|
|
5260
|
+
const hasRecoveryShare = useCallback4(
|
|
5261
|
+
() => run(() => socialHasRecoveryShare()),
|
|
5262
|
+
[socialHasRecoveryShare]
|
|
5263
|
+
);
|
|
5264
|
+
const shouldPromptRecovery = useCallback4(
|
|
5265
|
+
() => run(() => socialShouldPromptRecovery()),
|
|
5266
|
+
[socialShouldPromptRecovery]
|
|
5267
|
+
);
|
|
5268
|
+
const dismissRecoveryPrompt = useCallback4(
|
|
5269
|
+
() => run(() => socialDismissRecoveryPrompt()),
|
|
5270
|
+
[socialDismissRecoveryPrompt]
|
|
5271
|
+
);
|
|
4801
5272
|
return {
|
|
4802
5273
|
// State
|
|
4803
5274
|
isConnected: !!socialAuth.addresses,
|
|
@@ -4805,6 +5276,11 @@ function useSocialWallet() {
|
|
|
4805
5276
|
isNewUser: socialAuth.isNewUser,
|
|
4806
5277
|
addresses: socialAuth.addresses,
|
|
4807
5278
|
sessionExpiresAt: socialAuth.sessionExpiresAt,
|
|
5279
|
+
// True when addresses are known (from a previous visit) but there's no
|
|
5280
|
+
// live session — unlock() will always throw in this state (every
|
|
5281
|
+
// unlock requires a fresh JWT, and JWTs are never persisted). Route
|
|
5282
|
+
// this into a real reconnect (signInWithGoogle / sendEmailCode), not unlock().
|
|
5283
|
+
needsReconnect: socialAuth.needsReconnect,
|
|
4808
5284
|
// Modal
|
|
4809
5285
|
modalOpen: socialModalOpen,
|
|
4810
5286
|
openModal: openSocialModal,
|
|
@@ -4816,6 +5292,12 @@ function useSocialWallet() {
|
|
|
4816
5292
|
signMessage,
|
|
4817
5293
|
sendTransaction,
|
|
4818
5294
|
signSolanaTransaction: signSolanaTransaction2,
|
|
5295
|
+
// Recovery (2-of-3 Shamir share set)
|
|
5296
|
+
rotateShares,
|
|
5297
|
+
exportPortableBackup,
|
|
5298
|
+
hasRecoveryShare,
|
|
5299
|
+
shouldPromptRecovery,
|
|
5300
|
+
dismissRecoveryPrompt,
|
|
4819
5301
|
loading,
|
|
4820
5302
|
error,
|
|
4821
5303
|
clearError: () => setError(null)
|
|
@@ -5061,6 +5543,7 @@ export {
|
|
|
5061
5543
|
NewDeviceError,
|
|
5062
5544
|
OriginNotAllowedError,
|
|
5063
5545
|
PromptPinNotConfiguredError,
|
|
5546
|
+
ReconnectRequiredError,
|
|
5064
5547
|
RecoveryFailedError,
|
|
5065
5548
|
RecoveryWalletMismatchError,
|
|
5066
5549
|
SessionExpiredError,
|