Skip to content

Commit 115d093

Browse files
committed
Merge pull-request #524
2 parents 284eeaa + 7cffff0 commit 115d093

File tree

11 files changed

+142
-7
lines changed

11 files changed

+142
-7
lines changed

.changeset/tender-ways-fly.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@turnkey/sdk-server": minor
3+
"@turnkey/sdk-react": minor
4+
---
5+
6+
Adds wallet as an authentication option in the Embedded Wallet Kit components for sdk-react

.codesandbox/ci.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,6 @@
2121
"packages/react-native-passkey-stamper",
2222
"packages/telegram-cloud-storage-stamper"
2323
],
24-
"sandboxes": [],
24+
"sandboxes": ["/examples/react-components"],
2525
"node": "18"
2626
}

examples/react-components/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"@turnkey/sdk-browser": "workspace:*",
2121
"@turnkey/sdk-react": "workspace:*",
2222
"@turnkey/sdk-server": "workspace:*",
23+
"@turnkey/wallet-stamper": "workspace:*",
2324
"@types/node": "20.3.1",
2425
"@types/react": "18.2.14",
2526
"@types/react-dom": "18.2.6",

examples/react-components/src/app/layout.tsx

+3-1
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@
22

33
import "@turnkey/sdk-react/styles";
44
import { TurnkeyProvider, TurnkeyThemeProvider } from "@turnkey/sdk-react";
5-
5+
import { EthereumWallet } from "@turnkey/wallet-stamper";
6+
const wallet = new EthereumWallet();
67
const turnkeyConfig = {
78
apiBaseUrl: process.env.NEXT_PUBLIC_BASE_URL!,
89
defaultOrganizationId: process.env.NEXT_PUBLIC_ORGANIZATION_ID!,
910
rpId: process.env.NEXT_PUBLIC_RPID!,
1011
iframeUrl:
1112
process.env.NEXT_PUBLIC_AUTH_IFRAME_URL ?? "https://auth.turnkey.com",
13+
wallet,
1214
};
1315

1416
interface RootLayoutProps {

examples/react-components/src/app/page.tsx

+5
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ interface Config {
3737
email: boolean;
3838
passkey: boolean;
3939
phone: boolean;
40+
wallet: boolean;
4041
socials: SocialConfig;
4142
}
4243

@@ -51,12 +52,14 @@ export default function AuthPage() {
5152
"email",
5253
"phone",
5354
"passkey",
55+
"wallet",
5456
]);
5557

5658
const [config, setConfig] = useState<Config>({
5759
email: true,
5860
phone: false,
5961
passkey: true,
62+
wallet: false,
6063
socials: {
6164
enabled: true,
6265
providers: {
@@ -131,6 +134,7 @@ export default function AuthPage() {
131134
emailEnabled: config.email,
132135
passkeyEnabled: config.passkey,
133136
phoneEnabled: config.phone,
137+
walletEnabled: config.wallet,
134138
appleEnabled: config.socials.providers.apple,
135139
googleEnabled: config.socials.providers.google,
136140
facebookEnabled: config.socials.providers.facebook,
@@ -148,6 +152,7 @@ export default function AuthPage() {
148152
emailEnabled: config.email,
149153
passkeyEnabled: config.passkey,
150154
phoneEnabled: config.phone,
155+
walletEnabled: config.wallet,
151156
appleEnabled: config.socials.providers.apple,
152157
googleEnabled: config.socials.providers.google,
153158
facebookEnabled: config.socials.providers.facebook,

examples/with-sdk-server/tsconfig.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
"sourceMap": true,
1010
"target": "es2022",
1111
"types": ["node"],
12-
"outDir": "dist"
12+
"outDir": "dist",
13+
"skipLibCheck": true
1314
},
1415
"include": ["src/**/*"],
1516
"exclude": ["node_modules"]

packages/sdk-react/src/components/auth/Auth.tsx

+84-1
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ const passkeyIconError = (
5252
/>
5353
</svg>
5454
);
55+
5556
interface AuthProps {
5657
onAuthSuccess: () => Promise<void>;
5758
onError: (errorMessage: string) => void;
@@ -62,6 +63,7 @@ interface AuthProps {
6263
appleEnabled: boolean;
6364
facebookEnabled: boolean;
6465
googleEnabled: boolean;
66+
walletEnabled: boolean;
6567
sessionLengthSeconds?: number; // Desired expiration time in seconds for the generated API key
6668
googleClientId?: string; // will default to NEXT_PUBLIC_GOOGLE_CLIENT_ID
6769
appleClientId?: string; // will default to NEXT_PUBLIC_APPLE_CLIENT_ID
@@ -80,7 +82,8 @@ const Auth: React.FC<AuthProps> = ({
8082
customSmsMessage,
8183
customAccounts,
8284
}) => {
83-
const { passkeyClient, authIframeClient, turnkey } = useTurnkey();
85+
const { passkeyClient, authIframeClient, walletClient, turnkey } =
86+
useTurnkey();
8487
const [email, setEmail] = useState<string>("");
8588
const [phone, setPhone] = useState<string>("");
8689
const [otpId, setOtpId] = useState<string | null>(null);
@@ -92,6 +95,7 @@ const Auth: React.FC<AuthProps> = ({
9295
const [passkeySignupError, setPasskeySignupError] = useState("");
9396
const [loading, setLoading] = useState(true);
9497
const [passkeyCreated, setPasskeyCreated] = useState(false);
98+
const [walletLoading, setWalletLoading] = useState(false);
9599

96100
const handleResendCode = async () => {
97101
if (step === OtpType.Email) {
@@ -304,6 +308,63 @@ const Auth: React.FC<AuthProps> = ({
304308
}
305309
};
306310

311+
const handleLoginWithWallet = async () => {
312+
setWalletLoading(true);
313+
try {
314+
if (!walletClient) {
315+
throw new Error("Wallet client not initialized");
316+
}
317+
318+
const publicKey = await walletClient.getPublicKey();
319+
320+
if (!publicKey) {
321+
throw new Error(authErrors.wallet.noPublicKey);
322+
}
323+
324+
const { type } = walletClient.getWalletInterface();
325+
326+
const resp = await server.getOrCreateSuborg({
327+
filterType: FilterType.PublicKey,
328+
filterValue: publicKey,
329+
additionalData: {
330+
wallet: {
331+
publicKey,
332+
type,
333+
},
334+
},
335+
});
336+
337+
const suborgIds = resp?.subOrganizationIds;
338+
if (!suborgIds || suborgIds.length === 0) {
339+
onError(authErrors.wallet.loginFailed);
340+
return;
341+
}
342+
343+
const suborgId = suborgIds[0];
344+
345+
const sessionResponse = await walletClient.createReadWriteSession({
346+
targetPublicKey: authIframeClient?.iframePublicKey!,
347+
...(suborgId && { organizationId: suborgId }),
348+
...(authConfig.sessionLengthSeconds !== undefined && {
349+
expirationSeconds: authConfig.sessionLengthSeconds.toString(),
350+
}),
351+
});
352+
353+
if (sessionResponse?.credentialBundle) {
354+
await handleAuthSuccess(
355+
sessionResponse.credentialBundle,
356+
authConfig.sessionLengthSeconds?.toString(),
357+
);
358+
} else {
359+
throw new Error(authErrors.wallet.loginFailed);
360+
}
361+
} catch (error: any) {
362+
onError(error.message || authErrors.wallet.loginFailed);
363+
} finally {
364+
setWalletLoading(false);
365+
}
366+
};
367+
307368
const renderBackButton = () => (
308369
<ChevronLeftIcon
309370
onClick={() => {
@@ -475,6 +536,28 @@ const Auth: React.FC<AuthProps> = ({
475536
</div>
476537
) : null;
477538

539+
case "wallet":
540+
return authConfig.walletEnabled && !otpId ? (
541+
<div className={styles.passkeyContainer}>
542+
<button
543+
className={styles.authButton}
544+
type="button"
545+
onClick={handleLoginWithWallet}
546+
disabled={walletLoading}
547+
>
548+
{walletLoading ? (
549+
<CircularProgress
550+
size={24}
551+
thickness={4}
552+
className={styles.buttonProgress || ""}
553+
/>
554+
) : (
555+
"Continue with Wallet"
556+
)}
557+
</button>
558+
</div>
559+
) : null;
560+
478561
case "socials":
479562
return authConfig.googleEnabled ||
480563
authConfig.appleEnabled ||

packages/sdk-react/src/components/auth/constants.ts

+7
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@ export const authErrors = {
2929
loginFailed: "Failed to login with OIDC provider",
3030
},
3131

32+
// Wallet-related errors
33+
wallet: {
34+
loginFailed: "Failed to login with wallet",
35+
noPublicKey: "No public key found",
36+
},
37+
3238
// Sub-organization-related errors
3339
suborg: {
3440
fetchFailed: "Failed to fetch account",
@@ -45,4 +51,5 @@ export enum FilterType {
4551
Email = "EMAIL",
4652
PhoneNumber = "PHONE_NUMBER",
4753
OidcToken = "OIDC_TOKEN",
54+
PublicKey = "PUBLIC_KEY",
4855
}

packages/sdk-server/package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,11 @@
4444
"typecheck": "tsc -p tsconfig.typecheck.json"
4545
},
4646
"dependencies": {
47-
"cross-fetch": "^3.1.5",
4847
"@turnkey/api-key-stamper": "workspace:*",
4948
"@turnkey/http": "workspace:*",
49+
"@turnkey/wallet-stamper": "workspace:*",
5050
"buffer": "^6.0.3",
51+
"cross-fetch": "^3.1.5",
5152
"next": "^15.0.2"
5253
},
5354
"devDependencies": {

packages/sdk-server/src/actions.ts

+25-2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
DEFAULT_SOLANA_ACCOUNTS,
77
WalletAccount,
88
} from "./turnkey-helpers";
9+
import { WalletType } from "@turnkey/wallet-stamper";
910

1011
type GetOrCreateSuborgRequest = {
1112
filterType: FilterType;
@@ -16,13 +17,18 @@ type GetOrCreateSuborgRequest = {
1617
passkey?: Passkey;
1718
oauthProviders?: Provider[];
1819
customAccounts?: WalletAccount[];
20+
wallet?: {
21+
publicKey: string;
22+
type: WalletType;
23+
};
1924
};
2025
};
2126

2227
enum FilterType {
2328
Email = "EMAIL",
2429
PhoneNumber = "PHONE_NUMBER",
2530
OidcToken = "OIDC_TOKEN",
31+
PublicKey = "PUBLIC_KEY",
2632
}
2733

2834
type Session = {
@@ -85,6 +91,10 @@ type CreateSuborgRequest = {
8591
phoneNumber?: string | undefined;
8692
passkey?: Passkey | undefined;
8793
customAccounts?: WalletAccount[] | undefined;
94+
wallet?: {
95+
publicKey: string;
96+
type: WalletType;
97+
};
8898
};
8999

90100
type Passkey = {
@@ -284,7 +294,18 @@ export async function createSuborg(
284294
...(request.phoneNumber
285295
? { userPhoneNumber: request.phoneNumber }
286296
: {}),
287-
apiKeys: [],
297+
apiKeys: request.wallet
298+
? [
299+
{
300+
apiKeyName: `wallet-auth:${request.wallet.publicKey}`,
301+
publicKey: request.wallet.publicKey,
302+
curveType:
303+
request.wallet.type === WalletType.Ethereum
304+
? ("API_KEY_CURVE_SECP256K1" as const)
305+
: ("API_KEY_CURVE_ED25519" as const),
306+
},
307+
]
308+
: [],
288309
authenticators: request.passkey ? [request.passkey] : [],
289310
oauthProviders: request.oauthProviders ?? [],
290311
},
@@ -341,7 +362,6 @@ export async function getOrCreateSuborg(
341362
subOrganizationIds: suborgResponse.organizationIds!,
342363
};
343364
}
344-
345365
// No existing suborg found - create a new one
346366
const createPayload: CreateSuborgRequest = {
347367
...(request.additionalData?.email && {
@@ -359,6 +379,9 @@ export async function getOrCreateSuborg(
359379
...(request.additionalData?.customAccounts && {
360380
customAccounts: request.additionalData.customAccounts,
361381
}),
382+
...(request.additionalData?.wallet && {
383+
wallet: request.additionalData.wallet,
384+
}),
362385
};
363386

364387
const creationResponse = await createSuborg(createPayload);

pnpm-lock.yaml

+6
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)