diff --git a/.env.example b/.env.example index bedf6d7..241e656 100644 --- a/.env.example +++ b/.env.example @@ -16,4 +16,10 @@ TURNKEY_API_PUBLIC_KEY= TURNKEY_API_PRIVATE_KEY= # Use any randomly generated alphanumeric string -FACEBOOK_SECRET_SALT= \ No newline at end of file +FACEBOOK_SECRET_SALT= + + +WARCHEST_PRIVATE_KEY_ID="" +TURNKEY_WARCHEST_ORGANIZATION_ID="" +TURNKEY_WARCHEST_API_PUBLIC_KEY="" +TURNKEY_WARCHEST_API_PRIVATE_KEY="" diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..7c6782f --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,79 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Development Commands + +- `pnpm dev` - Start development server +- `pnpm build` - Build production bundle +- `pnpm start` - Start production server +- `pnpm lint` - Run ESLint +- `pnpm format` - Format code with Prettier +- `pnpm format:check` - Check code formatting + +## Architecture Overview + +This is a Next.js 14+ demo application showcasing Turnkey's embedded wallet functionality with multiple authentication methods. + +### Core Technologies +- **Framework**: Next.js 14 with App Router +- **Authentication**: Turnkey SDK for wallet-based auth, OAuth (Google, Apple, Facebook), email auth, passkeys +- **Blockchain**: Ethereum (Sepolia testnet) via Viem and Alchemy +- **UI**: shadcn/ui components with Radix UI primitives and Tailwind CSS +- **Type Safety**: TypeScript with Zod schema validation + +### Key Architecture Patterns + +**Provider Hierarchy** (`src/providers/index.tsx`): +``` +ThemeProvider > TurnkeyProvider > AuthProvider +``` + +**Authentication Flow**: +1. Multiple auth methods funnel through `AuthProvider` context +2. Creates Turnkey sub-organizations for users via server actions +3. Manages sessions and wallet state globally +4. Supports passkeys, OAuth providers, email magic links, and wallet imports + +**Server Actions Pattern** (`src/actions/`): +- `turnkey.ts` - Core Turnkey operations (user creation, auth, wallet management) +- `web3.ts` - Blockchain interactions (transactions, balance queries) +- All use "use server" directive for secure server-side operations + +**Environment Configuration** (`src/env.mjs`): +- Uses `@t3-oss/env-nextjs` for type-safe environment variables +- Separates client/server environment variables +- Validates all required Turnkey, OAuth, and blockchain API keys + +### Turnkey Integration Details + +**Configuration** (`src/config/turnkey.ts`): +- Manages iframe URLs for auth, export, and import flows +- Configures passkey settings with RP ID +- Sets up Ethereum RPC via Alchemy + +**Key Components**: +- Sub-organization creation per user with embedded wallets +- Warchest system for funding new wallets (0.01 ETH via `fundWallet`) +- Support for multiple authenticator types (passkeys, OAuth, API keys) +- Session management with expiry warnings + +**Wallet Operations**: +- Default Ethereum accounts created automatically +- Transaction history via Alchemy SDK +- Support for wallet export/import flows +- Address validation and balance checking + +### App Router Structure + +**Route Groups**: +- `(landing)` - Unauthenticated pages (login, OAuth callbacks) +- `(dashboard)` - Authenticated pages (wallet dashboard, settings) + +**Key Pages**: +- `/` - Landing page with authentication options +- `/dashboard` - Main wallet interface +- `/settings` - User settings and account management +- `/auth-client` - Standalone auth demonstration + +The application demonstrates a complete embedded wallet solution with multiple authentication methods, automatic wallet funding, and a polished user interface. \ No newline at end of file diff --git a/package.json b/package.json index 756cee3..3af4aa1 100644 --- a/package.json +++ b/package.json @@ -25,14 +25,14 @@ "@radix-ui/react-tooltip": "^1.1.2", "@react-oauth/google": "^0.12.1", "@t3-oss/env-nextjs": "^0.9.2", - "@turnkey/crypto": "^2.3.0", - "@turnkey/encoding": "^0.4.0", - "@turnkey/http": "^2.17.2", - "@turnkey/sdk-browser": "^1.11.1", - "@turnkey/sdk-react": "^2.0.3", - "@turnkey/sdk-server": "^1.7.2", - "@turnkey/viem": "^0.6.8", - "@turnkey/wallet-stamper": "^1.0.2", + "@turnkey/crypto": "^2.4.0", + "@turnkey/encoding": "^0.5.0", + "@turnkey/http": "^3.4.2", + "@turnkey/sdk-browser": "^5.2.1", + "@turnkey/sdk-react": "^5.2.1", + "@turnkey/sdk-server": "^4.1.1", + "@turnkey/viem": "^0.9.7", + "@turnkey/wallet-stamper": "^1.0.4", "@types/jsonwebtoken": "^9.0.7", "alchemy-sdk": "^3.4.1", "class-variance-authority": "^0.7.0", @@ -53,7 +53,7 @@ "tailwindcss-animate": "^1.0.7", "usehooks-ts": "^3.1.0", "vaul": "^0.9.4", - "viem": "^2.20.0", + "viem": "^2.31.1", "zod": "^3.23.4" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 853082a..a4f1dd1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -48,29 +48,29 @@ dependencies: specifier: ^0.9.2 version: 0.9.2(typescript@5.7.3)(zod@3.24.1) '@turnkey/crypto': - specifier: ^2.3.0 - version: 2.3.0 + specifier: ^2.4.0 + version: 2.4.0 '@turnkey/encoding': - specifier: ^0.4.0 - version: 0.4.0 + specifier: ^0.5.0 + version: 0.5.0 '@turnkey/http': - specifier: ^2.17.2 - version: 2.17.2 + specifier: ^3.4.2 + version: 3.4.2 '@turnkey/sdk-browser': - specifier: ^1.11.1 - version: 1.11.1(typescript@5.7.3)(zod@3.24.1) + specifier: ^5.2.1 + version: 5.2.1(typescript@5.7.3)(zod@3.24.1) '@turnkey/sdk-react': - specifier: ^2.0.3 - version: 2.0.3(@types/react@18.3.18)(prop-types@15.8.1)(react-dom@18.3.1)(react@18.3.1)(typescript@5.7.3)(zod@3.24.1) + specifier: ^5.2.1 + version: 5.2.1(@types/react@18.3.18)(prop-types@15.8.1)(react-dom@18.3.1)(react@18.3.1)(typescript@5.7.3)(zod@3.24.1) '@turnkey/sdk-server': - specifier: ^1.7.2 - version: 1.7.2 + specifier: ^4.1.1 + version: 4.1.1(typescript@5.7.3)(zod@3.24.1) '@turnkey/viem': - specifier: ^0.6.8 - version: 0.6.8(viem@2.22.13)(zod@3.24.1) + specifier: ^0.9.7 + version: 0.9.7(typescript@5.7.3)(viem@2.31.1)(zod@3.24.1) '@turnkey/wallet-stamper': - specifier: ^1.0.2 - version: 1.0.2(typescript@5.7.3)(zod@3.24.1) + specifier: ^1.0.4 + version: 1.0.4(typescript@5.7.3)(zod@3.24.1) '@types/jsonwebtoken': specifier: ^9.0.7 version: 9.0.8 @@ -132,8 +132,8 @@ dependencies: specifier: ^0.9.4 version: 0.9.9(@types/react-dom@18.3.5)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1) viem: - specifier: ^2.20.0 - version: 2.22.13(typescript@5.7.3)(zod@3.24.1) + specifier: ^2.31.1 + version: 2.31.1(typescript@5.7.3)(zod@3.24.1) zod: specifier: ^3.23.4 version: 3.24.1 @@ -268,8 +268,8 @@ packages: '@babel/helper-string-parser': 7.25.9 '@babel/helper-validator-identifier': 7.25.9 - /@emnapi/runtime@1.3.1: - resolution: {integrity: sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==} + /@emnapi/runtime@1.4.3: + resolution: {integrity: sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==} requiresBuild: true dependencies: tslib: 2.8.1 @@ -870,170 +870,187 @@ packages: react: 18.3.1 dev: false - /@img/sharp-darwin-arm64@0.33.5: - resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==} + /@img/sharp-darwin-arm64@0.34.2: + resolution: {integrity: sha512-OfXHZPppddivUJnqyKoi5YVeHRkkNE2zUFT2gbpKxp/JZCFYEYubnMg+gOp6lWfasPrTS+KPosKqdI+ELYVDtg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [darwin] requiresBuild: true optionalDependencies: - '@img/sharp-libvips-darwin-arm64': 1.0.4 + '@img/sharp-libvips-darwin-arm64': 1.1.0 dev: false optional: true - /@img/sharp-darwin-x64@0.33.5: - resolution: {integrity: sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==} + /@img/sharp-darwin-x64@0.34.2: + resolution: {integrity: sha512-dYvWqmjU9VxqXmjEtjmvHnGqF8GrVjM2Epj9rJ6BUIXvk8slvNDJbhGFvIoXzkDhrJC2jUxNLz/GUjjvSzfw+g==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [darwin] requiresBuild: true optionalDependencies: - '@img/sharp-libvips-darwin-x64': 1.0.4 + '@img/sharp-libvips-darwin-x64': 1.1.0 dev: false optional: true - /@img/sharp-libvips-darwin-arm64@1.0.4: - resolution: {integrity: sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==} + /@img/sharp-libvips-darwin-arm64@1.1.0: + resolution: {integrity: sha512-HZ/JUmPwrJSoM4DIQPv/BfNh9yrOA8tlBbqbLz4JZ5uew2+o22Ik+tHQJcih7QJuSa0zo5coHTfD5J8inqj9DA==} cpu: [arm64] os: [darwin] requiresBuild: true dev: false optional: true - /@img/sharp-libvips-darwin-x64@1.0.4: - resolution: {integrity: sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==} + /@img/sharp-libvips-darwin-x64@1.1.0: + resolution: {integrity: sha512-Xzc2ToEmHN+hfvsl9wja0RlnXEgpKNmftriQp6XzY/RaSfwD9th+MSh0WQKzUreLKKINb3afirxW7A0fz2YWuQ==} cpu: [x64] os: [darwin] requiresBuild: true dev: false optional: true - /@img/sharp-libvips-linux-arm64@1.0.4: - resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==} + /@img/sharp-libvips-linux-arm64@1.1.0: + resolution: {integrity: sha512-IVfGJa7gjChDET1dK9SekxFFdflarnUB8PwW8aGwEoF3oAsSDuNUTYS+SKDOyOJxQyDC1aPFMuRYLoDInyV9Ew==} cpu: [arm64] os: [linux] requiresBuild: true dev: false optional: true - /@img/sharp-libvips-linux-arm@1.0.5: - resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==} + /@img/sharp-libvips-linux-arm@1.1.0: + resolution: {integrity: sha512-s8BAd0lwUIvYCJyRdFqvsj+BJIpDBSxs6ivrOPm/R7piTs5UIwY5OjXrP2bqXC9/moGsyRa37eYWYCOGVXxVrA==} cpu: [arm] os: [linux] requiresBuild: true dev: false optional: true - /@img/sharp-libvips-linux-s390x@1.0.4: - resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==} + /@img/sharp-libvips-linux-ppc64@1.1.0: + resolution: {integrity: sha512-tiXxFZFbhnkWE2LA8oQj7KYR+bWBkiV2nilRldT7bqoEZ4HiDOcePr9wVDAZPi/Id5fT1oY9iGnDq20cwUz8lQ==} + cpu: [ppc64] + os: [linux] + requiresBuild: true + dev: false + optional: true + + /@img/sharp-libvips-linux-s390x@1.1.0: + resolution: {integrity: sha512-xukSwvhguw7COyzvmjydRb3x/09+21HykyapcZchiCUkTThEQEOMtBj9UhkaBRLuBrgLFzQ2wbxdeCCJW/jgJA==} cpu: [s390x] os: [linux] requiresBuild: true dev: false optional: true - /@img/sharp-libvips-linux-x64@1.0.4: - resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==} + /@img/sharp-libvips-linux-x64@1.1.0: + resolution: {integrity: sha512-yRj2+reB8iMg9W5sULM3S74jVS7zqSzHG3Ol/twnAAkAhnGQnpjj6e4ayUz7V+FpKypwgs82xbRdYtchTTUB+Q==} cpu: [x64] os: [linux] requiresBuild: true dev: false optional: true - /@img/sharp-libvips-linuxmusl-arm64@1.0.4: - resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==} + /@img/sharp-libvips-linuxmusl-arm64@1.1.0: + resolution: {integrity: sha512-jYZdG+whg0MDK+q2COKbYidaqW/WTz0cc1E+tMAusiDygrM4ypmSCjOJPmFTvHHJ8j/6cAGyeDWZOsK06tP33w==} cpu: [arm64] os: [linux] requiresBuild: true dev: false optional: true - /@img/sharp-libvips-linuxmusl-x64@1.0.4: - resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==} + /@img/sharp-libvips-linuxmusl-x64@1.1.0: + resolution: {integrity: sha512-wK7SBdwrAiycjXdkPnGCPLjYb9lD4l6Ze2gSdAGVZrEL05AOUJESWU2lhlC+Ffn5/G+VKuSm6zzbQSzFX/P65A==} cpu: [x64] os: [linux] requiresBuild: true dev: false optional: true - /@img/sharp-linux-arm64@0.33.5: - resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==} + /@img/sharp-linux-arm64@0.34.2: + resolution: {integrity: sha512-D8n8wgWmPDakc83LORcfJepdOSN6MvWNzzz2ux0MnIbOqdieRZwVYY32zxVx+IFUT8er5KPcyU3XXsn+GzG/0Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] requiresBuild: true optionalDependencies: - '@img/sharp-libvips-linux-arm64': 1.0.4 + '@img/sharp-libvips-linux-arm64': 1.1.0 dev: false optional: true - /@img/sharp-linux-arm@0.33.5: - resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==} + /@img/sharp-linux-arm@0.34.2: + resolution: {integrity: sha512-0DZzkvuEOqQUP9mo2kjjKNok5AmnOr1jB2XYjkaoNRwpAYMDzRmAqUIa1nRi58S2WswqSfPOWLNOr0FDT3H5RQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] requiresBuild: true optionalDependencies: - '@img/sharp-libvips-linux-arm': 1.0.5 + '@img/sharp-libvips-linux-arm': 1.1.0 dev: false optional: true - /@img/sharp-linux-s390x@0.33.5: - resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==} + /@img/sharp-linux-s390x@0.34.2: + resolution: {integrity: sha512-EGZ1xwhBI7dNISwxjChqBGELCWMGDvmxZXKjQRuqMrakhO8QoMgqCrdjnAqJq/CScxfRn+Bb7suXBElKQpPDiw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] requiresBuild: true optionalDependencies: - '@img/sharp-libvips-linux-s390x': 1.0.4 + '@img/sharp-libvips-linux-s390x': 1.1.0 dev: false optional: true - /@img/sharp-linux-x64@0.33.5: - resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==} + /@img/sharp-linux-x64@0.34.2: + resolution: {integrity: sha512-sD7J+h5nFLMMmOXYH4DD9UtSNBD05tWSSdWAcEyzqW8Cn5UxXvsHAxmxSesYUsTOBmUnjtxghKDl15EvfqLFbQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] requiresBuild: true optionalDependencies: - '@img/sharp-libvips-linux-x64': 1.0.4 + '@img/sharp-libvips-linux-x64': 1.1.0 dev: false optional: true - /@img/sharp-linuxmusl-arm64@0.33.5: - resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==} + /@img/sharp-linuxmusl-arm64@0.34.2: + resolution: {integrity: sha512-NEE2vQ6wcxYav1/A22OOxoSOGiKnNmDzCYFOZ949xFmrWZOVII1Bp3NqVVpvj+3UeHMFyN5eP/V5hzViQ5CZNA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] requiresBuild: true optionalDependencies: - '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 + '@img/sharp-libvips-linuxmusl-arm64': 1.1.0 dev: false optional: true - /@img/sharp-linuxmusl-x64@0.33.5: - resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==} + /@img/sharp-linuxmusl-x64@0.34.2: + resolution: {integrity: sha512-DOYMrDm5E6/8bm/yQLCWyuDJwUnlevR8xtF8bs+gjZ7cyUNYXiSf/E8Kp0Ss5xasIaXSHzb888V1BE4i1hFhAA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] requiresBuild: true optionalDependencies: - '@img/sharp-libvips-linuxmusl-x64': 1.0.4 + '@img/sharp-libvips-linuxmusl-x64': 1.1.0 dev: false optional: true - /@img/sharp-wasm32@0.33.5: - resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==} + /@img/sharp-wasm32@0.34.2: + resolution: {integrity: sha512-/VI4mdlJ9zkaq53MbIG6rZY+QRN3MLbR6usYlgITEzi4Rpx5S6LFKsycOQjkOGmqTNmkIdLjEvooFKwww6OpdQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [wasm32] requiresBuild: true dependencies: - '@emnapi/runtime': 1.3.1 + '@emnapi/runtime': 1.4.3 + dev: false + optional: true + + /@img/sharp-win32-arm64@0.34.2: + resolution: {integrity: sha512-cfP/r9FdS63VA5k0xiqaNaEoGxBg9k7uE+RQGzuK9fHt7jib4zAVVseR9LsE4gJcNWgT6APKMNnCcnyOtmSEUQ==} + engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} + cpu: [arm64] + os: [win32] + requiresBuild: true dev: false optional: true - /@img/sharp-win32-ia32@0.33.5: - resolution: {integrity: sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==} + /@img/sharp-win32-ia32@0.34.2: + resolution: {integrity: sha512-QLjGGvAbj0X/FXl8n1WbtQ6iVBpWU7JO94u/P2M4a8CFYsvQi4GW2mRy/JqkRx0qpBzaOdKJKw8uc930EX2AHw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ia32] os: [win32] @@ -1041,8 +1058,8 @@ packages: dev: false optional: true - /@img/sharp-win32-x64@0.33.5: - resolution: {integrity: sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==} + /@img/sharp-win32-x64@0.34.2: + resolution: {integrity: sha512-aUdT6zEYtDKCaxkofmmJDJYGCf0+pJg3eU9/oBuqvEeoB9dKI6ZLc/1iLJCTuJQDO4ptntAlkUmHgGjyuobZbw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [win32] @@ -1252,8 +1269,8 @@ packages: resolution: {integrity: sha512-7CnQyD5G8shHxQIIg3c7/pSeYFeMhsNbpU/bmvH7ZnDql7mNRgg8O2JZrhrc/soFnfBnKP4/xXNiiSIPn2w8gA==} dev: false - /@next/env@15.1.6: - resolution: {integrity: sha512-d9AFQVPEYNr+aqokIiPLNK/MTyt3DWa/dpKveiAaVccUadFbhFEvY6FXYX2LJO2Hv7PHnLBu2oWwB4uBuHjr/w==} + /@next/env@15.3.3: + resolution: {integrity: sha512-OdiMrzCl2Xi0VTjiQQUK0Xh7bJHnOuET2s+3V+Y40WJBAXrJeGA3f+I8MZJ/YQ3mVGi5XGR1L66oFlgqXhQ4Vw==} dev: false /@next/eslint-plugin-next@14.1.0: @@ -1271,8 +1288,8 @@ packages: dev: false optional: true - /@next/swc-darwin-arm64@15.1.6: - resolution: {integrity: sha512-u7lg4Mpl9qWpKgy6NzEkz/w0/keEHtOybmIl0ykgItBxEM5mYotS5PmqTpo+Rhg8FiOiWgwr8USxmKQkqLBCrw==} + /@next/swc-darwin-arm64@15.3.3: + resolution: {integrity: sha512-WRJERLuH+O3oYB4yZNVahSVFmtxRNjNF1I1c34tYMoJb0Pve+7/RaLAJJizyYiFhjYNGHRAE1Ri2Fd23zgDqhg==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] @@ -1289,8 +1306,8 @@ packages: dev: false optional: true - /@next/swc-darwin-x64@15.1.6: - resolution: {integrity: sha512-x1jGpbHbZoZ69nRuogGL2MYPLqohlhnT9OCU6E6QFewwup+z+M6r8oU47BTeJcWsF2sdBahp5cKiAcDbwwK/lg==} + /@next/swc-darwin-x64@15.3.3: + resolution: {integrity: sha512-XHdzH/yBc55lu78k/XwtuFR/ZXUTcflpRXcsu0nKmF45U96jt1tsOZhVrn5YH+paw66zOANpOnFQ9i6/j+UYvw==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] @@ -1307,8 +1324,8 @@ packages: dev: false optional: true - /@next/swc-linux-arm64-gnu@15.1.6: - resolution: {integrity: sha512-jar9sFw0XewXsBzPf9runGzoivajeWJUc/JkfbLTC4it9EhU8v7tCRLH7l5Y1ReTMN6zKJO0kKAGqDk8YSO2bg==} + /@next/swc-linux-arm64-gnu@15.3.3: + resolution: {integrity: sha512-VZ3sYL2LXB8znNGcjhocikEkag/8xiLgnvQts41tq6i+wql63SMS1Q6N8RVXHw5pEUjiof+II3HkDd7GFcgkzw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] @@ -1325,8 +1342,8 @@ packages: dev: false optional: true - /@next/swc-linux-arm64-musl@15.1.6: - resolution: {integrity: sha512-+n3u//bfsrIaZch4cgOJ3tXCTbSxz0s6brJtU3SzLOvkJlPQMJ+eHVRi6qM2kKKKLuMY+tcau8XD9CJ1OjeSQQ==} + /@next/swc-linux-arm64-musl@15.3.3: + resolution: {integrity: sha512-h6Y1fLU4RWAp1HPNJWDYBQ+e3G7sLckyBXhmH9ajn8l/RSMnhbuPBV/fXmy3muMcVwoJdHL+UtzRzs0nXOf9SA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] @@ -1343,8 +1360,8 @@ packages: dev: false optional: true - /@next/swc-linux-x64-gnu@15.1.6: - resolution: {integrity: sha512-SpuDEXixM3PycniL4iVCLyUyvcl6Lt0mtv3am08sucskpG0tYkW1KlRhTgj4LI5ehyxriVVcfdoxuuP8csi3kQ==} + /@next/swc-linux-x64-gnu@15.3.3: + resolution: {integrity: sha512-jJ8HRiF3N8Zw6hGlytCj5BiHyG/K+fnTKVDEKvUCyiQ/0r5tgwO7OgaRiOjjRoIx2vwLR+Rz8hQoPrnmFbJdfw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] @@ -1361,8 +1378,8 @@ packages: dev: false optional: true - /@next/swc-linux-x64-musl@15.1.6: - resolution: {integrity: sha512-L4druWmdFSZIIRhF+G60API5sFB7suTbDRhYWSjiw0RbE+15igQvE2g2+S973pMGvwN3guw7cJUjA/TmbPWTHQ==} + /@next/swc-linux-x64-musl@15.3.3: + resolution: {integrity: sha512-HrUcTr4N+RgiiGn3jjeT6Oo208UT/7BuTr7K0mdKRBtTbT4v9zJqCDKO97DUqqoBK1qyzP1RwvrWTvU6EPh/Cw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] @@ -1379,8 +1396,8 @@ packages: dev: false optional: true - /@next/swc-win32-arm64-msvc@15.1.6: - resolution: {integrity: sha512-s8w6EeqNmi6gdvM19tqKKWbCyOBvXFbndkGHl+c9YrzsLARRdCHsD9S1fMj8gsXm9v8vhC8s3N8rjuC/XrtkEg==} + /@next/swc-win32-arm64-msvc@15.3.3: + resolution: {integrity: sha512-SxorONgi6K7ZUysMtRF3mIeHC5aA3IQLmKFQzU0OuhuUYwpOBc1ypaLJLP5Bf3M9k53KUUUj4vTPwzGvl/NwlQ==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] @@ -1406,8 +1423,8 @@ packages: dev: false optional: true - /@next/swc-win32-x64-msvc@15.1.6: - resolution: {integrity: sha512-6xomMuu54FAFxttYr5PJbEfu96godcxBTRk1OhAvJq0/EnmFU/Ybiax30Snis4vdWZ9LGpf7Roy5fSs7v/5ROQ==} + /@next/swc-win32-x64-msvc@15.3.3: + resolution: {integrity: sha512-4QZG6F8enl9/S2+yIiOiju0iCTFd93d8VC1q9LZS4p/Xuk81W2QDjCFeoogmrWWkAD59z8ZxepBQap2dKS5ruw==} engines: {node: '>= 10'} cpu: [x64] os: [win32] @@ -1424,12 +1441,24 @@ packages: engines: {node: ^14.21.3 || >=16} dev: false + /@noble/ciphers@1.3.0: + resolution: {integrity: sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==} + engines: {node: ^14.21.3 || >=16} + dev: false + /@noble/curves@1.4.0: resolution: {integrity: sha512-p+4cb332SFCrReJkCYe8Xzm0OWi4Jji5jVdIZRL/PmacmDkFNw6MrrV+gGpiPxLHbV+zKFRywUWbaseT+tZRXg==} dependencies: '@noble/hashes': 1.4.0 dev: false + /@noble/curves@1.8.0: + resolution: {integrity: sha512-j84kjAbzEnQHaSIhRPUmB3/eVXu2k3dKPl2LOrR8fSOIL+89U+7lV117EWHtq/GHM3ReGHM46iRBdZfpc4HRUQ==} + engines: {node: ^14.21.3 || >=16} + dependencies: + '@noble/hashes': 1.7.0 + dev: false + /@noble/curves@1.8.1: resolution: {integrity: sha512-warwspo+UYUPep0Q+vtdVB4Ugn8GGQj8iyB3gnRWsztmUHTI3S1nhdiWNsPUGL0vud7JlRRk1XEu7Lq1KGTnMQ==} engines: {node: ^14.21.3 || >=16} @@ -1437,16 +1466,33 @@ packages: '@noble/hashes': 1.7.1 dev: false + /@noble/curves@1.9.1: + resolution: {integrity: sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==} + engines: {node: ^14.21.3 || >=16} + dependencies: + '@noble/hashes': 1.8.0 + dev: false + /@noble/hashes@1.4.0: resolution: {integrity: sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==} engines: {node: '>= 16'} dev: false + /@noble/hashes@1.7.0: + resolution: {integrity: sha512-HXydb0DgzTpDPwbVeDGCG1gIu7X6+AuU6Zl6av/E/KG8LMsvPntvq+w17CHRpKBmN6Ybdrt1eP3k4cj8DJa78w==} + engines: {node: ^14.21.3 || >=16} + dev: false + /@noble/hashes@1.7.1: resolution: {integrity: sha512-B8XBPsn4vT/KJAGqDzbwztd+6Yte3P4V7iafm24bxgDe/mlRuK6xmWPuCNrKt2vDafZ8MfJLlchDG/vYafQEjQ==} engines: {node: ^14.21.3 || >=16} dev: false + /@noble/hashes@1.8.0: + resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} + engines: {node: ^14.21.3 || >=16} + dev: false + /@nodelib/fs.scandir@2.1.5: resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -2187,23 +2233,23 @@ packages: resolution: {integrity: sha512-kkKUDVlII2DQiKy7UstOR1ErJP8kUKAQ4oa+SQtM0K+lPdmmjj0YnnxBgtTVYH7mUKtbsxeFC9y0AmK7Yb78/A==} dev: true - /@scure/base@1.2.4: - resolution: {integrity: sha512-5Yy9czTO47mqz+/J8GM6GIId4umdCk1wc1q8rKERQulIoc8VP9pzDcghv10Tl2E7R96ZUx/PhND3ESYUQX8NuQ==} + /@scure/base@1.2.6: + resolution: {integrity: sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==} dev: false - /@scure/bip32@1.6.2: - resolution: {integrity: sha512-t96EPDMbtGgtb7onKKqxRLfE5g05k7uHnHRM2xdE6BP/ZmxaLtPek4J4KfVn/90IQNrU1IOAqMgiDtUdtbe3nw==} + /@scure/bip32@1.7.0: + resolution: {integrity: sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==} dependencies: - '@noble/curves': 1.8.1 - '@noble/hashes': 1.7.1 - '@scure/base': 1.2.4 + '@noble/curves': 1.9.1 + '@noble/hashes': 1.8.0 + '@scure/base': 1.2.6 dev: false - /@scure/bip39@1.5.4: - resolution: {integrity: sha512-TFM4ni0vKvCfBpohoh+/lY05i9gRbSwXWngAsF4CABQxoaOHijxuaZ2R6cStDQ5CHtHO9aGJTr4ksVJASRRyMA==} + /@scure/bip39@1.6.0: + resolution: {integrity: sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==} dependencies: - '@noble/hashes': 1.7.1 - '@scure/base': 1.2.4 + '@noble/hashes': 1.8.0 + '@scure/base': 1.2.6 dev: false /@swc/counter@0.1.3: @@ -2262,64 +2308,72 @@ packages: zod: 3.24.1 dev: false - /@turnkey/api-key-stamper@0.4.4: - resolution: {integrity: sha512-KXvf2yV3OkSrZYEnHSgOwy2XS7Q4+XNwRkgRhDNsiYvgz8KNGLbqXg0/aAlkTIUT7NYODZ3iaGMml0DtyzGeDQ==} + /@turnkey/api-key-stamper@0.4.6: + resolution: {integrity: sha512-zUcbhOunsSD//CIKAbfyX9FWchiHcO/CjOdBKvbB+8w6gk/m4NxchVrYCdqdrtxzPFdRYAdFjcUnN0FBvDKobw==} engines: {node: '>=18.0.0'} dependencies: '@noble/curves': 1.8.1 - '@turnkey/encoding': 0.4.0 + '@turnkey/encoding': 0.5.0 sha256-uint8array: 0.10.7 dev: false - /@turnkey/crypto@2.3.0: - resolution: {integrity: sha512-RdsSn9nKzr2Ik8TssvQ6qwPuUzUfbV+G0u3XYTaGxAHS8ifWtgvwkiWRLRqy0Y89xh7GaU+OmFjwCO9qwBoWfQ==} + /@turnkey/crypto@2.4.0: + resolution: {integrity: sha512-jh3uE2W8sUvrI0KQsJ8SF/pvTeumhE3YmFoNB7lcqzIfvamNWYCgg3iWoA64W7U3uEzcFtNBefZsWySsVcl2Ew==} engines: {node: '>=18.0.0'} dependencies: '@noble/ciphers': 0.5.3 '@noble/curves': 1.4.0 '@noble/hashes': 1.4.0 - '@turnkey/encoding': 0.4.0 - bs58: 5.0.0 + bs58: 6.0.0 bs58check: 3.0.1 dev: false - /@turnkey/encoding@0.4.0: - resolution: {integrity: sha512-ptLgcpWVt34KTPx0omF2QLJrosW6I//clCJ4G2+yngYFCzrdR0yBchV/BOcfME67mK1v3MmauyXl9AAnQTmB4Q==} + /@turnkey/encoding@0.5.0: + resolution: {integrity: sha512-nRlKRQa6B5/xltGUKN1iKo4h4YC/0iFz0fAuFFZevc+YGDj7ddAP/3HkWmVvLmdoicUgs9rxvWbLRlgqPkbwzQ==} engines: {node: '>=18.0.0'} dev: false - /@turnkey/http@2.17.2: - resolution: {integrity: sha512-0hqh0cZFSI5egkdqawsalzWOfOfHyCtjFHcmpyYJJqWMJnB0hkfFjKfPbgMBsiejZhwttmhqWstWFyhbxo8SNw==} + /@turnkey/http@3.4.2: + resolution: {integrity: sha512-UFJxnEjOSXBmmozsUrBBRMqA9ErCqMen4VkYu+C5cyDAAKgCq01fNEtp+CakD+npd35WbtfevIW5q7JdJ6nP0w==} engines: {node: '>=16.0.0'} dependencies: - '@turnkey/api-key-stamper': 0.4.4 - '@turnkey/encoding': 0.4.0 - '@turnkey/webauthn-stamper': 0.5.0 + '@turnkey/api-key-stamper': 0.4.6 + '@turnkey/encoding': 0.5.0 + '@turnkey/webauthn-stamper': 0.5.1 cross-fetch: 3.2.0 transitivePeerDependencies: - encoding dev: false - /@turnkey/iframe-stamper@2.1.0: - resolution: {integrity: sha512-QdxEfIMi/9wgddBr00pKmWk0QvjZA+D8QKunK0IzlaXsXYuqiSiJVfz2QQnn9HGcQOW5UcEWRAWRaSbwrrqqUA==} + /@turnkey/iframe-stamper@2.5.0: + resolution: {integrity: sha512-XjntbA5CNjxGRH+loceAlVLL9PG9Q4Y7p5zjBm4DeKclhD6lpUl9h8INArMEXIFbfLwLjjS6Q+SmQG4BHvNY6A==} engines: {node: '>=18.0.0'} dev: false - /@turnkey/sdk-browser@1.11.1(typescript@5.7.3)(zod@3.24.1): - resolution: {integrity: sha512-BVgnHRrtSgSj2XB3NmofcGIqznkw6raOggj/8vVFv8H0o2saVV8qSOpvDNqo2ELTUvTMaPA/wFNW9sNLYkZJ0A==} + /@turnkey/indexed-db-stamper@1.1.0: + resolution: {integrity: sha512-tgGtHT7reAqSrO+aOAwfpVIln+N7nk81yKXJ1e1+5qv9PfJQjE8+mMxolinCAIZwIqsaFBtjdjm6NCW+Me7rcw==} engines: {node: '>=18.0.0'} dependencies: - '@turnkey/api-key-stamper': 0.4.4 - '@turnkey/crypto': 2.3.0 - '@turnkey/encoding': 0.4.0 - '@turnkey/http': 2.17.2 - '@turnkey/iframe-stamper': 2.1.0 - '@turnkey/wallet-stamper': 1.0.2(typescript@5.7.3)(zod@3.24.1) - '@turnkey/webauthn-stamper': 0.5.0 + '@turnkey/api-key-stamper': 0.4.6 + '@turnkey/encoding': 0.5.0 + dev: false + + /@turnkey/sdk-browser@5.2.1(typescript@5.7.3)(zod@3.24.1): + resolution: {integrity: sha512-z3edKbYjY20e8gEIIuy9JYRr005D0oNa3BI54UJce+yadQax4M+2bYeJG3WVk94mKGZqTAz+j1BEqSccxwa9rg==} + engines: {node: '>=18.0.0'} + dependencies: + '@turnkey/api-key-stamper': 0.4.6 + '@turnkey/crypto': 2.4.0 + '@turnkey/encoding': 0.5.0 + '@turnkey/http': 3.4.2 + '@turnkey/iframe-stamper': 2.5.0 + '@turnkey/indexed-db-stamper': 1.1.0 + '@turnkey/sdk-types': 0.1.0 + '@turnkey/wallet-stamper': 1.0.4(typescript@5.7.3)(zod@3.24.1) + '@turnkey/webauthn-stamper': 0.5.1 bs58check: 3.0.1 buffer: 6.0.3 cross-fetch: 3.2.0 - elliptic: 6.6.1 hpke-js: 1.6.1 transitivePeerDependencies: - bufferutil @@ -2329,8 +2383,8 @@ packages: - zod dev: false - /@turnkey/sdk-react@2.0.3(@types/react@18.3.18)(prop-types@15.8.1)(react-dom@18.3.1)(react@18.3.1)(typescript@5.7.3)(zod@3.24.1): - resolution: {integrity: sha512-1PuEg8l6vS7InvZxF3zWKpZEHmNLD2F5Rnx5X8Ux41rdxx3DZu2th1h04IIqjt+aD39XFc8YB1F9a1SDclzP5Q==} + /@turnkey/sdk-react@5.2.1(@types/react@18.3.18)(prop-types@15.8.1)(react-dom@18.3.1)(react@18.3.1)(typescript@5.7.3)(zod@3.24.1): + resolution: {integrity: sha512-+vWeeQWLQlcxkrguDFyQRwE+Ei2RQJUPe2NUkbPaTvWFIlxNgj/KEHrtiZMwWT1gO8Zj4rm03tDKy8OcsyLYwg==} engines: {node: '>=18.0.0'} peerDependencies: '@types/react': ^18.2.75 @@ -2343,17 +2397,19 @@ packages: '@mui/material': 6.4.1(@emotion/react@11.14.0)(@emotion/styled@11.14.0)(@types/react@18.3.18)(react-dom@18.3.1)(react@18.3.1) '@noble/hashes': 1.4.0 '@react-oauth/google': 0.12.1(react-dom@18.3.1)(react@18.3.1) - '@turnkey/crypto': 2.3.0 - '@turnkey/sdk-browser': 1.11.1(typescript@5.7.3)(zod@3.24.1) - '@turnkey/sdk-server': 1.7.2 - '@turnkey/wallet-stamper': 1.0.2(typescript@5.7.3)(zod@3.24.1) + '@turnkey/crypto': 2.4.0 + '@turnkey/sdk-browser': 5.2.1(typescript@5.7.3)(zod@3.24.1) + '@turnkey/sdk-server': 4.1.1(typescript@5.7.3)(zod@3.24.1) + '@turnkey/sdk-types': 0.1.0 + '@turnkey/wallet-stamper': 1.0.4(typescript@5.7.3)(zod@3.24.1) '@types/react': 18.3.18 + jwt-decode: 4.0.0 libphonenumber-js: 1.11.18 - next: 15.1.6(react-dom@18.3.1)(react@18.3.1) + next: 15.3.3(react-dom@18.3.1)(react@18.3.1) react: 18.3.1 react-apple-login: 1.1.6(prop-types@15.8.1)(react-dom@18.3.1)(react@18.3.1) react-international-phone: 4.5.0(react@18.3.1) - usehooks-ts: 3.1.0(react@18.3.1) + usehooks-ts: 3.1.1(react@18.3.1) transitivePeerDependencies: - '@babel/core' - '@mui/material-pigment-css' @@ -2372,46 +2428,56 @@ packages: - zod dev: false - /@turnkey/sdk-server@1.7.2: - resolution: {integrity: sha512-VdyMuJnJHuyF7qPmNlLIx960WVskVdGT5bA7WFJvZIcysRSxNV5KNihCsHz3Y4OPPFMJAcMIK3dAnlyZZfIKlA==} + /@turnkey/sdk-server@4.1.1(typescript@5.7.3)(zod@3.24.1): + resolution: {integrity: sha512-q1dDWC0pCR4vdWGJAQmGMc34gnc8FpockCnobQwRkinTAsYzZc4nDlDCafcgxghIGBDsd+pB2AcaGdAxaKwlIg==} engines: {node: '>=18.0.0'} dependencies: - '@turnkey/api-key-stamper': 0.4.4 - '@turnkey/http': 2.17.2 + '@turnkey/api-key-stamper': 0.4.6 + '@turnkey/http': 3.4.2 + '@turnkey/wallet-stamper': 1.0.4(typescript@5.7.3)(zod@3.24.1) buffer: 6.0.3 cross-fetch: 3.2.0 - elliptic: 6.6.1 transitivePeerDependencies: + - bufferutil - encoding + - typescript + - utf-8-validate + - zod + dev: false + + /@turnkey/sdk-types@0.1.0: + resolution: {integrity: sha512-kpgXigg3fMG6xLDoE3F79M7kCpIsNJi8zqzVJEJwyY1Xtpw5SnEbCIuw3NEp35yst3S1+D4OszUc9K2l0E0qLA==} + engines: {node: '>=18.0.0'} dev: false - /@turnkey/viem@0.6.8(viem@2.22.13)(zod@3.24.1): - resolution: {integrity: sha512-76X1QlKNtDfyYGZ1t8aza+pH7cUFTdWSjhme3EVKf2ew2qYxcE4SlgbgNV9BLYprsr098+WD1J9XP5mMlORw7g==} + /@turnkey/viem@0.9.7(typescript@5.7.3)(viem@2.31.1)(zod@3.24.1): + resolution: {integrity: sha512-6evwwjFh7kR88hJPsahdWjkDHWbQ0TpAra88zK2I7p24mh19rydoKJ23SjOvnhcAuMNbkHQRyliD4Lh/1eoAFw==} engines: {node: '>=18.0.0'} peerDependencies: - viem: ^1.16.6 || ^2.1.1 + viem: ^1.16.6 || ^2.24.2 dependencies: - '@turnkey/api-key-stamper': 0.4.4 - '@turnkey/http': 2.17.2 - '@turnkey/sdk-browser': 1.11.1(typescript@5.7.3)(zod@3.24.1) - '@turnkey/sdk-server': 1.7.2 + '@noble/curves': 1.8.0 + '@turnkey/api-key-stamper': 0.4.6 + '@turnkey/http': 3.4.2 + '@turnkey/sdk-browser': 5.2.1(typescript@5.7.3)(zod@3.24.1) + '@turnkey/sdk-server': 4.1.1(typescript@5.7.3)(zod@3.24.1) cross-fetch: 4.1.0 - typescript: 5.7.3 - viem: 2.22.13(typescript@5.7.3)(zod@3.24.1) + viem: 2.31.1(typescript@5.7.3)(zod@3.24.1) transitivePeerDependencies: - bufferutil - encoding + - typescript - utf-8-validate - zod dev: false - /@turnkey/wallet-stamper@1.0.2(typescript@5.7.3)(zod@3.24.1): - resolution: {integrity: sha512-C4Lup8yN90Qn3HUTCZ0RiRwIWT1y9oqV5wqXmYaBPlaV52DC0N3LJl20Nh3vZu9Had79Rnz/OBm+ZWDAHi5SCg==} + /@turnkey/wallet-stamper@1.0.4(typescript@5.7.3)(zod@3.24.1): + resolution: {integrity: sha512-xSkkQh30SzhYFv/2Q0+oARZbReSqzwjxi6j3mbmOn/udDyjYjz9mdV7rm+rN4+iwDD64ZjSIJTK0flotYjIWHw==} dependencies: - '@turnkey/crypto': 2.3.0 - '@turnkey/encoding': 0.4.0 + '@turnkey/crypto': 2.4.0 + '@turnkey/encoding': 0.5.0 optionalDependencies: - viem: 2.22.13(typescript@5.7.3)(zod@3.24.1) + viem: 2.31.1(typescript@5.7.3)(zod@3.24.1) transitivePeerDependencies: - bufferutil - typescript @@ -2419,8 +2485,8 @@ packages: - zod dev: false - /@turnkey/webauthn-stamper@0.5.0: - resolution: {integrity: sha512-iUbTUwD4f4ibdLy5PWWb7ITEz4S4VAP9/mNjFhoRY3cKVVTDfmykrVTKjPOIHWzDgAmLtgrLvySIIC9ZBVENBw==} + /@turnkey/webauthn-stamper@0.5.1: + resolution: {integrity: sha512-eBwceTStSSettBQsLo3X5eJEarcK9f20cGUdi6jOesXOP86iYEIgR4+aH2qyCQ3eaovj+Hl44UGngXueIm/tKg==} engines: {node: '>=18.0.0'} dependencies: sha256-uint8array: 0.10.7 @@ -2820,6 +2886,10 @@ packages: resolution: {integrity: sha512-FuwxlW4H5kh37X/oW59pwTzzTKRzfrrQwhmyspRM7swOEZcHtDZSCt45U6oKgtuFE+WYPblePMVIPR4RZrh/hw==} dev: false + /base-x@5.0.1: + resolution: {integrity: sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==} + dev: false + /base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} dev: false @@ -2879,10 +2949,16 @@ packages: base-x: 4.0.0 dev: false + /bs58@6.0.0: + resolution: {integrity: sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==} + dependencies: + base-x: 5.0.1 + dev: false + /bs58check@3.0.1: resolution: {integrity: sha512-hjuuJvoWEybo7Hn/0xOrczQKKEKD63WguEjlhLExYs2wUBcebDC1jDNK17eEAD2lYfw82d5ASC1d7K3SWszjaQ==} dependencies: - '@noble/hashes': 1.4.0 + '@noble/hashes': 1.7.1 bs58: 5.0.0 dev: false @@ -3175,8 +3251,8 @@ packages: engines: {node: '>=0.4.0'} dev: false - /detect-libc@2.0.3: - resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} + /detect-libc@2.0.4: + resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} engines: {node: '>=8'} requiresBuild: true dev: false @@ -3254,18 +3330,6 @@ packages: minimalistic-crypto-utils: 1.0.1 dev: false - /elliptic@6.6.1: - resolution: {integrity: sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==} - dependencies: - bn.js: 4.12.1 - brorand: 1.1.0 - hash.js: 1.1.7 - hmac-drbg: 1.0.1 - inherits: 2.0.4 - minimalistic-assert: 1.0.1 - minimalistic-crypto-utils: 1.0.1 - dev: false - /emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -4350,12 +4414,12 @@ packages: /isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - /isows@1.0.6(ws@8.18.0): - resolution: {integrity: sha512-lPHCayd40oW98/I0uvgaHKWCSvkzY27LjWLbtzOm64yQ+G3Q5npjjbdppU65iZXkK1Zt+kH9pfegli0AYfwYYw==} + /isows@1.0.7(ws@8.18.2): + resolution: {integrity: sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg==} peerDependencies: ws: '*' dependencies: - ws: 8.18.0 + ws: 8.18.2 dev: false /iterator.prototype@1.1.5: @@ -4473,6 +4537,11 @@ packages: safe-buffer: 5.2.1 dev: false + /jwt-decode@4.0.0: + resolution: {integrity: sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==} + engines: {node: '>=18'} + dev: false + /keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} dependencies: @@ -4714,8 +4783,8 @@ packages: - babel-plugin-macros dev: false - /next@15.1.6(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-Hch4wzbaX0vKQtalpXvUiw5sYivBy4cm5rzUKrBnUB/y436LGrvOUqYvlSeNVCWFO/770gDlltR9gqZH62ct4Q==} + /next@15.3.3(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-JqNj29hHNmCLtNvd090SyRbXJiivQ+58XjCcrC50Crb5g5u2zi7Y2YivbsEfzk6AtVI80akdOQbaMZwWB1Hthw==} engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} hasBin: true peerDependencies: @@ -4735,7 +4804,7 @@ packages: sass: optional: true dependencies: - '@next/env': 15.1.6 + '@next/env': 15.3.3 '@swc/counter': 0.1.3 '@swc/helpers': 0.5.15 busboy: 1.6.0 @@ -4745,15 +4814,15 @@ packages: react-dom: 18.3.1(react@18.3.1) styled-jsx: 5.1.6(react@18.3.1) optionalDependencies: - '@next/swc-darwin-arm64': 15.1.6 - '@next/swc-darwin-x64': 15.1.6 - '@next/swc-linux-arm64-gnu': 15.1.6 - '@next/swc-linux-arm64-musl': 15.1.6 - '@next/swc-linux-x64-gnu': 15.1.6 - '@next/swc-linux-x64-musl': 15.1.6 - '@next/swc-win32-arm64-msvc': 15.1.6 - '@next/swc-win32-x64-msvc': 15.1.6 - sharp: 0.33.5 + '@next/swc-darwin-arm64': 15.3.3 + '@next/swc-darwin-x64': 15.3.3 + '@next/swc-linux-arm64-gnu': 15.3.3 + '@next/swc-linux-arm64-musl': 15.3.3 + '@next/swc-linux-x64-gnu': 15.3.3 + '@next/swc-linux-x64-musl': 15.3.3 + '@next/swc-win32-arm64-msvc': 15.3.3 + '@next/swc-win32-x64-msvc': 15.3.3 + sharp: 0.34.2 transitivePeerDependencies: - '@babel/core' - babel-plugin-macros @@ -4884,8 +4953,8 @@ packages: safe-push-apply: 1.0.0 dev: true - /ox@0.6.7(typescript@5.7.3)(zod@3.24.1): - resolution: {integrity: sha512-17Gk/eFsFRAZ80p5eKqv89a57uXjd3NgIf1CaXojATPBuujVc/fQSVhBeAU9JCRB+k7J50WQAyWTxK19T9GgbA==} + /ox@0.7.1(typescript@5.7.3)(zod@3.24.1): + resolution: {integrity: sha512-+k9fY9PRNuAMHRFIUbiK9Nt5seYHHzSQs9Bj+iMETcGtlpS7SmBzcGSVUQO3+nqGLEiNK4598pHNFlVRaZbRsg==} peerDependencies: typescript: '>=5.4.0' peerDependenciesMeta: @@ -4893,10 +4962,11 @@ packages: optional: true dependencies: '@adraffy/ens-normalize': 1.11.0 - '@noble/curves': 1.8.1 - '@noble/hashes': 1.7.1 - '@scure/bip32': 1.6.2 - '@scure/bip39': 1.5.4 + '@noble/ciphers': 1.3.0 + '@noble/curves': 1.9.1 + '@noble/hashes': 1.8.0 + '@scure/bip32': 1.7.0 + '@scure/bip39': 1.6.0 abitype: 1.0.8(typescript@5.7.3)(zod@3.24.1) eventemitter3: 5.0.1 typescript: 5.7.3 @@ -5431,6 +5501,14 @@ packages: engines: {node: '>=10'} hasBin: true + /semver@7.7.2: + resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} + engines: {node: '>=10'} + hasBin: true + requiresBuild: true + dev: false + optional: true + /set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -5466,34 +5544,36 @@ packages: resolution: {integrity: sha512-1Q6JQU4tX9NqsDGodej6pkrUVQVNapLZnvkwIhddH/JqzBZF1fSaxSWNY6sziXBE8aEa2twtGkXUrwzGeZCMpQ==} dev: false - /sharp@0.33.5: - resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==} + /sharp@0.34.2: + resolution: {integrity: sha512-lszvBmB9QURERtyKT2bNmsgxXK0ShJrL/fvqlonCo7e6xBF8nT8xU6pW+PMIbLsz0RxQk3rgH9kd8UmvOzlMJg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} requiresBuild: true dependencies: color: 4.2.3 - detect-libc: 2.0.3 - semver: 7.6.3 + detect-libc: 2.0.4 + semver: 7.7.2 optionalDependencies: - '@img/sharp-darwin-arm64': 0.33.5 - '@img/sharp-darwin-x64': 0.33.5 - '@img/sharp-libvips-darwin-arm64': 1.0.4 - '@img/sharp-libvips-darwin-x64': 1.0.4 - '@img/sharp-libvips-linux-arm': 1.0.5 - '@img/sharp-libvips-linux-arm64': 1.0.4 - '@img/sharp-libvips-linux-s390x': 1.0.4 - '@img/sharp-libvips-linux-x64': 1.0.4 - '@img/sharp-libvips-linuxmusl-arm64': 1.0.4 - '@img/sharp-libvips-linuxmusl-x64': 1.0.4 - '@img/sharp-linux-arm': 0.33.5 - '@img/sharp-linux-arm64': 0.33.5 - '@img/sharp-linux-s390x': 0.33.5 - '@img/sharp-linux-x64': 0.33.5 - '@img/sharp-linuxmusl-arm64': 0.33.5 - '@img/sharp-linuxmusl-x64': 0.33.5 - '@img/sharp-wasm32': 0.33.5 - '@img/sharp-win32-ia32': 0.33.5 - '@img/sharp-win32-x64': 0.33.5 + '@img/sharp-darwin-arm64': 0.34.2 + '@img/sharp-darwin-x64': 0.34.2 + '@img/sharp-libvips-darwin-arm64': 1.1.0 + '@img/sharp-libvips-darwin-x64': 1.1.0 + '@img/sharp-libvips-linux-arm': 1.1.0 + '@img/sharp-libvips-linux-arm64': 1.1.0 + '@img/sharp-libvips-linux-ppc64': 1.1.0 + '@img/sharp-libvips-linux-s390x': 1.1.0 + '@img/sharp-libvips-linux-x64': 1.1.0 + '@img/sharp-libvips-linuxmusl-arm64': 1.1.0 + '@img/sharp-libvips-linuxmusl-x64': 1.1.0 + '@img/sharp-linux-arm': 0.34.2 + '@img/sharp-linux-arm64': 0.34.2 + '@img/sharp-linux-s390x': 0.34.2 + '@img/sharp-linux-x64': 0.34.2 + '@img/sharp-linuxmusl-arm64': 0.34.2 + '@img/sharp-linuxmusl-x64': 0.34.2 + '@img/sharp-wasm32': 0.34.2 + '@img/sharp-win32-arm64': 0.34.2 + '@img/sharp-win32-ia32': 0.34.2 + '@img/sharp-win32-x64': 0.34.2 dev: false optional: true @@ -6003,6 +6083,16 @@ packages: react: 18.3.1 dev: false + /usehooks-ts@3.1.1(react@18.3.1): + resolution: {integrity: sha512-I4diPp9Cq6ieSUH2wu+fDAVQO43xwtulo+fKEidHUwZPnYImbtkTjzIJYcDcJqxgmX31GVqNFURodvcgHcW0pA==} + engines: {node: '>=16.15.0'} + peerDependencies: + react: ^16.8.0 || ^17 || ^18 || ^19 || ^19.0.0-rc + dependencies: + lodash.debounce: 4.0.8 + react: 18.3.1 + dev: false + /utf-8-validate@5.0.10: resolution: {integrity: sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==} engines: {node: '>=6.14.2'} @@ -6028,23 +6118,23 @@ packages: - '@types/react-dom' dev: false - /viem@2.22.13(typescript@5.7.3)(zod@3.24.1): - resolution: {integrity: sha512-MaQKY5DUQ5SnZJPMytp5nTgvRu7N3wzvBhY31/9VT4lxDZAcQolqYEK3EqP+cdAD8jl0YmGuoJlfW9D1crqlGg==} + /viem@2.31.1(typescript@5.7.3)(zod@3.24.1): + resolution: {integrity: sha512-w7P+glrmZm4xd3wugH31lXy2J98DAWlEKW+legUgiADvfOT2wjDIGywsEtEWk44auQLykTiijtNz8pEnINa/8A==} peerDependencies: typescript: '>=5.0.4' peerDependenciesMeta: typescript: optional: true dependencies: - '@noble/curves': 1.8.1 - '@noble/hashes': 1.7.1 - '@scure/bip32': 1.6.2 - '@scure/bip39': 1.5.4 + '@noble/curves': 1.9.1 + '@noble/hashes': 1.8.0 + '@scure/bip32': 1.7.0 + '@scure/bip39': 1.6.0 abitype: 1.0.8(typescript@5.7.3)(zod@3.24.1) - isows: 1.0.6(ws@8.18.0) - ox: 0.6.7(typescript@5.7.3)(zod@3.24.1) + isows: 1.0.7(ws@8.18.2) + ox: 0.7.1(typescript@5.7.3)(zod@3.24.1) typescript: 5.7.3 - ws: 8.18.0 + ws: 8.18.2 transitivePeerDependencies: - bufferutil - utf-8-validate @@ -6173,8 +6263,8 @@ packages: optional: true dev: false - /ws@8.18.0: - resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==} + /ws@8.18.2: + resolution: {integrity: sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==} engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 diff --git a/src/actions/turnkey.ts b/src/actions/turnkey.ts index 57e39a0..bda2351 100644 --- a/src/actions/turnkey.ts +++ b/src/actions/turnkey.ts @@ -1,5 +1,6 @@ "use server" +import { OtpType } from "@turnkey/sdk-react" import { ApiKeyStamper, DEFAULT_ETHEREUM_ACCOUNTS, @@ -184,34 +185,45 @@ export const createUserSubOrg = async ({ export const oauth = async ({ credential, - targetPublicKey, - targetSubOrgId, + publicKey, + subOrgId, }: { credential: string - targetPublicKey: string - targetSubOrgId: string + publicKey: string + subOrgId: string }) => { - const oauthResponse = await client.oauth({ + const oauthResponse = await client.oauthLogin({ oidcToken: credential, - targetPublicKey, - organizationId: targetSubOrgId, + publicKey, + organizationId: subOrgId, }) - return oauthResponse + return { + userId: oauthResponse.activity.votes?.[0]?.userId, + session: oauthResponse.session, + organizationId: subOrgId, + } } -const getMagicLinkTemplate = (action: string, email: string, method: string) => - `${siteConfig.url.base}/email-${action}?userEmail=${email}&continueWith=${method}&credentialBundle=%s` +const getMagicLinkTemplate = ( + action: string, + email: string, + method: string, + publicKey: string, + baseUrl: string = siteConfig.url.base +) => + `${baseUrl}/email-${action}?userEmail=${email}&continueWith=${method}&publicKey=${publicKey}&credentialBundle=%s` export const initEmailAuth = async ({ email, targetPublicKey, + baseUrl, }: { email: Email targetPublicKey: string + baseUrl?: string }) => { let organizationId = await getSubOrgIdByEmail(email as Email) - if (!organizationId) { const { subOrg } = await createUserSubOrg({ email: email as Email, @@ -219,22 +231,68 @@ export const initEmailAuth = async ({ organizationId = subOrg.subOrganizationId } - const magicLinkTemplate = getMagicLinkTemplate("auth", email, "email") + const magicLinkTemplate = getMagicLinkTemplate( + "auth", + email, + "email", + targetPublicKey, + baseUrl + ) if (organizationId?.length) { - const authResponse = await client.emailAuth({ - email, - targetPublicKey, - organizationId, + const authResponse = await client.initOtp({ + userIdentifier: targetPublicKey, + otpType: OtpType.Email, + contact: email, emailCustomization: { magicLinkTemplate, }, }) - return authResponse } } +export const verifyOtp = async ({ + otpId, + otpCode, + publicKey, +}: { + otpId: string + otpCode: string + publicKey: string +}) => { + const authResponse = await client.verifyOtp({ + otpId, + otpCode, + }) + + return authResponse +} + +export const otpLogin = async ({ + publicKey, + verificationToken, + email, +}: { + publicKey: string + verificationToken: string + email: Email +}) => { + const subOrgId = await getSubOrgIdByEmail(email) + + const sessionResponse = await client.otpLogin({ + verificationToken, + publicKey, + organizationId: subOrgId, + }) + + return { + userId: sessionResponse.activity.votes[0]?.userId, + session: sessionResponse.session, + organizationId: subOrgId, + } +} + type EmailParam = { email: Email } type PublicKeyParam = { publicKey: string } type UsernameParam = { username: string } @@ -310,13 +368,15 @@ export async function getWalletsWithAccounts( }) const accountsWithBalance = await Promise.all( - accounts.map(async ({ address, ...account }) => { - return { - ...account, - address: getAddress(address), - balance: undefined, - } - }) + accounts + .filter((account) => account.curve === "CURVE_SECP256K1") + .map(async ({ address, ...account }) => { + return { + ...account, + address: getAddress(address), + balance: undefined, + } + }) ) return { ...wallet, accounts: accountsWithBalance } }) @@ -376,7 +436,7 @@ const warchestClient = new TurnkeyServerClient({ }) export const fundWallet = async (address: Address) => { - const value = parseEther("0.01") + const value = parseEther("1") const { receivedTransactions } = await getTransactions(address) if (receivedTransactions.length >= 1) { diff --git a/src/app/(dashboard)/layout.tsx b/src/app/(dashboard)/layout.tsx index 037d605..8d621ec 100644 --- a/src/app/(dashboard)/layout.tsx +++ b/src/app/(dashboard)/layout.tsx @@ -13,7 +13,6 @@ export default function DashboardLayout({
-
{children}
diff --git a/src/app/(landing)/email-auth/page.tsx b/src/app/(landing)/email-auth/page.tsx index 716b871..9b2507f 100644 --- a/src/app/(landing)/email-auth/page.tsx +++ b/src/app/(landing)/email-auth/page.tsx @@ -17,17 +17,20 @@ import { Icons } from "@/components/icons" function EmailAuthContent() { const searchParams = useSearchParams() const { completeEmailAuth } = useAuth() - const { authIframeClient } = useTurnkey() + const { indexedDbClient } = useTurnkey() const userEmail = searchParams.get("userEmail") const continueWith = searchParams.get("continueWith") const credentialBundle = searchParams.get("credentialBundle") useEffect(() => { - // Ensure that the authIframeClient is available before attempting to complete the email auth - if (authIframeClient && userEmail && continueWith && credentialBundle) { - completeEmailAuth({ userEmail, continueWith, credentialBundle }) + if (userEmail && continueWith && credentialBundle && indexedDbClient) { + completeEmailAuth({ + userEmail, + continueWith, + credentialBundle, + }) } - }, [authIframeClient]) + }, [userEmail, continueWith, credentialBundle, indexedDbClient]) return (
diff --git a/src/app/(landing)/layout.tsx b/src/app/(landing)/layout.tsx index 4733b5e..c8e2407 100644 --- a/src/app/(landing)/layout.tsx +++ b/src/app/(landing)/layout.tsx @@ -12,18 +12,20 @@ interface LandingLayoutProps { export default function LandingLayout({ children }: LandingLayoutProps) { return ( -
-
- gradient - -
-
- {children} - +
+
+
+ gradient + +
+
+ {children} + +
) diff --git a/src/app/(landing)/oauth-callback/apple/page.tsx b/src/app/(landing)/oauth-callback/apple/page.tsx index 917b8d9..dc36660 100644 --- a/src/app/(landing)/oauth-callback/apple/page.tsx +++ b/src/app/(landing)/oauth-callback/apple/page.tsx @@ -10,7 +10,6 @@ import { Card, CardHeader, CardTitle } from "@/components/ui/card" import { Icons } from "@/components/icons" function OAuthProcessCallback() { - const { authIframeClient } = useTurnkey() const { loginWithApple } = useAuth() const router = useRouter() @@ -37,7 +36,7 @@ function OAuthProcessCallback() { // Trigger loginWithOAuth when both token and iframePublicKey are available, but only once useEffect(() => { - if (storedToken && authIframeClient?.iframePublicKey && !hasLoggedIn) { + if (storedToken && !hasLoggedIn) { try { // Call the OAuth login function with the stored token loginWithApple(storedToken) @@ -49,12 +48,7 @@ function OAuthProcessCallback() { router.push(`/?error=${encodeURIComponent(msg)}`) } } - }, [ - storedToken, - authIframeClient?.iframePublicKey, - hasLoggedIn, - loginWithApple, - ]) + }, [storedToken, hasLoggedIn, loginWithApple]) return (
diff --git a/src/app/(landing)/oauth-callback/facebook/page.tsx b/src/app/(landing)/oauth-callback/facebook/page.tsx index dd13bda..cd581d8 100644 --- a/src/app/(landing)/oauth-callback/facebook/page.tsx +++ b/src/app/(landing)/oauth-callback/facebook/page.tsx @@ -15,7 +15,6 @@ import { verifierSegmentToChallenge } from "../../../../lib/facebook-utils" function FacebookProcessCallback() { const searchParams = useSearchParams() - const { authIframeClient } = useTurnkey() const { loginWithFacebook } = useAuth() const [storedCode, setStoredCode] = useState(null) @@ -69,19 +68,13 @@ function FacebookProcessCallback() { } } - if ( - storedCode && - storedState && - authIframeClient?.iframePublicKey && - !hasLoggedIn - ) { + if (storedCode && storedState && !hasLoggedIn) { // Call the async handler to exchange the token handleTokenExchange() } }, [ storedCode, storedState, - authIframeClient?.iframePublicKey, hasLoggedIn, loginWithFacebook, setHasLoggedIn, diff --git a/src/components/account.tsx b/src/components/account.tsx index ed69a30..ed89dcb 100644 --- a/src/components/account.tsx +++ b/src/components/account.tsx @@ -71,6 +71,7 @@ export default function Account() { const handleNewAccount = (e: Event) => { e.preventDefault() e.stopPropagation() + newWalletAccount() } @@ -136,7 +137,7 @@ export default function Account() {
- {user?.username} + {user?.name} {user?.email || ""} diff --git a/src/components/add-passkey.tsx b/src/components/add-passkey.tsx index 0b9005d..7d9375f 100644 --- a/src/components/add-passkey.tsx +++ b/src/components/add-passkey.tsx @@ -19,7 +19,7 @@ export default function AddPasskey({ }: { onPasskeyAdded: (authenticatorId: string) => void }) { - const { passkeyClient, client } = useTurnkey() + const { passkeyClient, indexedDbClient } = useTurnkey() const { user } = useUser() const [open, setOpen] = useState(false) const [passkeyName, setPasskeyName] = useState("") @@ -35,23 +35,24 @@ export default function AddPasskey({ name: "Turnkey - Demo Embedded Wallet", }, user: { - name: user?.username, - displayName: user?.username, + name: user?.name, + displayName: user?.name, }, }, }) if (credential) { - const authenticatorsResponse = await client?.createAuthenticators({ - authenticators: [ - { - authenticatorName: passkeyName, - challenge: credential.encodedChallenge, - attestation: credential.attestation, - }, - ], - userId: `${user?.userId}`, - }) + const authenticatorsResponse = + await indexedDbClient?.createAuthenticators({ + authenticators: [ + { + authenticatorName: passkeyName, + challenge: credential.encodedChallenge, + attestation: credential.attestation, + }, + ], + userId: `${user?.id}`, + }) if (authenticatorsResponse?.activity.id) { toast.success("Passkey added!") diff --git a/src/components/apple-auth.tsx b/src/components/apple-auth.tsx index e0a42c4..04b62a4 100644 --- a/src/components/apple-auth.tsx +++ b/src/components/apple-auth.tsx @@ -16,20 +16,26 @@ const AppleAuth = () => { const clientId = env.NEXT_PUBLIC_APPLE_OAUTH_CLIENT_ID const redirectURI = `${siteConfig.url.base}/oauth-callback/apple` - const { authIframeClient } = useTurnkey() + const { indexedDbClient } = useTurnkey() const [nonce, setNonce] = useState("") // Generate nonce based on iframePublicKey useEffect(() => { - if (authIframeClient?.iframePublicKey) { - const hashedPublicKey = sha256( - authIframeClient.iframePublicKey as `0x${string}` - ).replace(/^0x/, "") + const getPublicKey = async () => { + const publicKey = await indexedDbClient?.getPublicKey() + if (publicKey) { + const hashedPublicKey = sha256(publicKey as `0x${string}`).replace( + /^0x/, + "" + ) - setNonce(hashedPublicKey) + setNonce(hashedPublicKey) + } } - }, [authIframeClient?.iframePublicKey]) + + getPublicKey() + }, [indexedDbClient]) return ( <> diff --git a/src/components/auth.tsx b/src/components/auth.tsx index 01254d0..b7bafdc 100644 --- a/src/components/auth.tsx +++ b/src/components/auth.tsx @@ -11,6 +11,7 @@ import * as z from "zod" import { Email } from "@/types/turnkey" import { useUser } from "@/hooks/use-user" +import { Badge } from "@/components/ui/badge" import { LoadingButton } from "@/components/ui/button.loader" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { @@ -88,7 +89,15 @@ function AuthContent() { <> - +
+ + + Demo + +
Log in or sign up diff --git a/src/components/export-wallet.tsx b/src/components/export-wallet.tsx index e7d07f2..b75c4c9 100644 --- a/src/components/export-wallet.tsx +++ b/src/components/export-wallet.tsx @@ -28,7 +28,7 @@ export default function ExportWalletDialog({ }) { const { state } = useWallets() const { selectedWallet, selectedAccount } = state - const { turnkey, client } = useTurnkey() + const { turnkey, indexedDbClient } = useTurnkey() const { export: exportConfig } = turnkeyConfig.iFrame const [iframeClient, setIframeClient] = useState( null @@ -101,16 +101,16 @@ export default function ExportWalletDialog({ const exportSeedPhrase = async () => { if (iframeClient && selectedWallet) { - const exportResponse = await client?.exportWallet({ + const exportResponse = await indexedDbClient?.exportWallet({ walletId: selectedWallet.walletId, targetPublicKey: `${iframeClient?.iframePublicKey}`, }) if (exportResponse?.exportBundle) { - const currentUser = await turnkey?.getCurrentUser() + const session = await turnkey?.getSession() const response = await iframeClient?.injectWalletExportBundle( exportResponse.exportBundle, - `${currentUser?.organization.organizationId}` + `${session?.organizationId}` ) setInjectResponse(response) @@ -120,15 +120,15 @@ export default function ExportWalletDialog({ const exportPrivateKey = async () => { if (iframeClient && selectedAccount) { - const exportResponse = await client?.exportWalletAccount({ + const exportResponse = await indexedDbClient?.exportWalletAccount({ address: selectedAccount.address, targetPublicKey: `${iframeClient?.iframePublicKey}`, }) if (exportResponse?.exportBundle) { - const currentUser = await turnkey?.getCurrentUser() + const session = await turnkey?.getSession() const response = await iframeClient?.injectKeyExportBundle( exportResponse.exportBundle, - `${currentUser?.organization.organizationId}` + `${session?.organizationId}` ) setInjectResponse(response) diff --git a/src/components/facebook-auth.tsx b/src/components/facebook-auth.tsx index 566667d..4102cbc 100644 --- a/src/components/facebook-auth.tsx +++ b/src/components/facebook-auth.tsx @@ -13,7 +13,7 @@ import { Button } from "./ui/button" import { Skeleton } from "./ui/skeleton" const FacebookAuth = () => { - const { authIframeClient } = useTurnkey() + const { indexedDbClient } = useTurnkey() const [nonce, setNonce] = useState("") @@ -25,14 +25,20 @@ const FacebookAuth = () => { // Generate nonce based on iframePublicKey useEffect(() => { - if (authIframeClient?.iframePublicKey) { - const hashedPublicKey = sha256( - authIframeClient.iframePublicKey as `0x${string}` - ).replace(/^0x/, "") - - setNonce(hashedPublicKey) + const getPublicKey = async () => { + const publicKey = await indexedDbClient?.getPublicKey() + if (publicKey) { + const hashedPublicKey = sha256(publicKey as `0x${string}`).replace( + /^0x/, + "" + ) + + setNonce(hashedPublicKey) + } } - }, [authIframeClient?.iframePublicKey]) + + getPublicKey() + }, [indexedDbClient]) const redirectToFacebook = async () => { const { verifier, codeChallenge } = await generateChallengePair() diff --git a/src/components/google-auth.tsx b/src/components/google-auth.tsx index f5b1553..6e57532 100644 --- a/src/components/google-auth.tsx +++ b/src/components/google-auth.tsx @@ -7,29 +7,37 @@ import { GoogleLogin, GoogleOAuthProvider, } from "@react-oauth/google" +import { uncompressRawPublicKey } from "@turnkey/crypto" import { useTurnkey } from "@turnkey/sdk-react" -import { sha256 } from "viem" +import { sha256, toHex } from "viem" import { env } from "@/env.mjs" import { Skeleton } from "./ui/skeleton" const GoogleAuth = () => { - const { authIframeClient } = useTurnkey() + const { indexedDbClient } = useTurnkey() const clientId = env.NEXT_PUBLIC_GOOGLE_OAUTH_CLIENT_ID const [nonce, setNonce] = useState("") const { loginWithGoogle } = useAuth() useEffect(() => { - if (authIframeClient?.iframePublicKey) { - const hashedPublicKey = sha256( - authIframeClient.iframePublicKey as `0x${string}` - ).replace(/^0x/, "") + const getPublicKey = async () => { + const publicKey = await indexedDbClient?.getPublicKey() - setNonce(hashedPublicKey) + if (publicKey) { + const hashedPublicKey = sha256(publicKey as `0x${string}`).replace( + /^0x/, + "" + ) + + setNonce(hashedPublicKey) + } } - }, [authIframeClient?.iframePublicKey]) + + getPublicKey() + }, [indexedDbClient]) const onSuccess = (credentialResponse: CredentialResponse) => { if (credentialResponse.credential) { diff --git a/src/components/import-wallet.tsx b/src/components/import-wallet.tsx index 5601ebf..c052d11 100644 --- a/src/components/import-wallet.tsx +++ b/src/components/import-wallet.tsx @@ -32,9 +32,7 @@ export default function ImportWalletDialog({ }: { children: React.ReactNode }) { - const { state } = useWallets() - const { selectedWallet, selectedAccount } = state - const { turnkey, getActiveClient, authIframeClient } = useTurnkey() + const { turnkey, indexedDbClient } = useTurnkey() const { import: importConfig } = turnkeyConfig.iFrame const [iframeClient, setIframeClient] = useState( null @@ -93,18 +91,17 @@ export default function ImportWalletDialog({ } const initImportWallet = async () => { - const currentUser = await turnkey?.getCurrentUser() - const activeClient = await getActiveClient() + const session = await turnkey?.getSession() - const initImportResponse = await activeClient?.initImportWallet({ - userId: `${currentUser?.userId}`, + const initImportResponse = await indexedDbClient?.initImportWallet({ + userId: `${session?.userId}`, }) if (initImportResponse?.importBundle) { const injectResponse = await iframeClient?.injectImportBundle( initImportResponse.importBundle, - `${currentUser?.organization.organizationId}`, - `${currentUser?.userId}` + `${session?.organizationId}`, + `${session?.userId}` ) if (injectResponse) { @@ -114,13 +111,12 @@ export default function ImportWalletDialog({ } const importWallet = async () => { - const currentUser = await turnkey?.getCurrentUser() - const activeClient = await getActiveClient() + const session = await turnkey?.getSession() const encryptedBundle = await iframeClient?.extractWalletEncryptedBundle() if (encryptedBundle) { - const importResponse = await activeClient?.importWallet({ - userId: `${currentUser?.userId}`, + const importResponse = await indexedDbClient?.importWallet({ + userId: `${session?.userId}`, walletName: importName, encryptedBundle, accounts: DEFAULT_ETHEREUM_ACCOUNTS, @@ -133,18 +129,17 @@ export default function ImportWalletDialog({ } const initImportPrivateKey = async () => { - const currentUser = await turnkey?.getCurrentUser() - const activeClient = await getActiveClient() + const session = await turnkey?.getSession() - const initImportResponse = await activeClient?.initImportPrivateKey({ - userId: `${currentUser?.userId}`, + const initImportResponse = await indexedDbClient?.initImportPrivateKey({ + userId: `${session?.userId}`, }) if (initImportResponse?.importBundle) { const injectResponse = await iframeClient?.injectImportBundle( initImportResponse.importBundle, - `${currentUser?.organization.organizationId}`, - `${currentUser?.userId}` + `${session?.organizationId}`, + `${session?.userId}` ) if (injectResponse) { @@ -154,14 +149,13 @@ export default function ImportWalletDialog({ } const importPrivateKey = async () => { - const currentUser = await turnkey?.getCurrentUser() - const activeClient = await getActiveClient() + const session = await turnkey?.getSession() const encryptedBundle = await iframeClient?.extractKeyEncryptedBundle() if (encryptedBundle) { - const importResponse = await activeClient?.importPrivateKey({ - userId: `${currentUser?.userId}`, + const importResponse = await indexedDbClient?.importPrivateKey({ + userId: `${session?.userId}`, privateKeyName: importName, encryptedBundle: encryptedBundle, // TODO: Add support for other curves like solana diff --git a/src/components/nav-menu.tsx b/src/components/nav-menu.tsx index b2f7880..52895a4 100644 --- a/src/components/nav-menu.tsx +++ b/src/components/nav-menu.tsx @@ -2,13 +2,19 @@ import Link from "next/link" import Account from "./account" import { Icons } from "./icons" +import { Badge } from "./ui/badge" export default function NavMenu() { return (
- - - +
+ + + + + Demo + +
diff --git a/src/components/passkeys.tsx b/src/components/passkeys.tsx index 1b72852..c4320c6 100644 --- a/src/components/passkeys.tsx +++ b/src/components/passkeys.tsx @@ -12,7 +12,7 @@ import AddPasskey from "./add-passkey" import { PasskeyItem } from "./passkey-item" export function Passkeys() { - const { client } = useTurnkey() + const { indexedDbClient } = useTurnkey() const { user } = useUser() const [authenticators, setAuthenticators] = useState([]) const [loading, setLoading] = useState(true) @@ -20,7 +20,7 @@ export function Passkeys() { useEffect(() => { if (user) { setLoading(true) - getAuthenticators(user.userId, user.organization.organizationId).then( + getAuthenticators(user.id, user.organization.organizationId).then( (authenticators) => { setAuthenticators(authenticators) setLoading(false) @@ -30,8 +30,8 @@ export function Passkeys() { }, [user]) const removeAuthenticator = async (authenticatorId: string) => { - const authenticatorResponse = await client?.deleteAuthenticators({ - userId: `${user?.userId}`, + const authenticatorResponse = await indexedDbClient?.deleteAuthenticators({ + userId: `${user?.id}`, authenticatorIds: [authenticatorId], }) if (authenticatorResponse) { diff --git a/src/components/transfer-dialog.tsx b/src/components/transfer-dialog.tsx index a81fded..2422458 100644 --- a/src/components/transfer-dialog.tsx +++ b/src/components/transfer-dialog.tsx @@ -52,7 +52,7 @@ export default function TransferDialog() { const { state } = useWallets() const { selectedAccount } = state const { ethPrice } = useTokenPrice() - const { client } = useTurnkey() + const { turnkey, indexedDbClient } = useTurnkey() const { addPendingTransaction } = useTransactions() const isDesktop = useMediaQuery("(min-width: 564px)") const isClient = useIsClient() @@ -91,17 +91,17 @@ export default function TransferDialog() { useEffect(() => { const initializeWalletClient = async () => { - if (!selectedAccount || !client) return + if (!selectedAccount || !indexedDbClient) return const walletClient = await getTurnkeyWalletClient( - client, + indexedDbClient, selectedAccount.address ) setWalletClient(walletClient) } initializeWalletClient() - }, [selectedAccount, client]) + }, [selectedAccount, indexedDbClient]) useEffect(() => { const ethAmountParsed = parseFloat(ethAmount || "0") diff --git a/src/config/site.ts b/src/config/site.ts index 7a0bc3e..b890931 100644 --- a/src/config/site.ts +++ b/src/config/site.ts @@ -1,9 +1,22 @@ import { SiteConfig } from "@/types" -const baseUrl = - process.env.NEXT_PUBLIC_VERCEL_ENV === "production" - ? `https://${process.env.NEXT_PUBLIC_VERCEL_PROJECT_PRODUCTION_URL}` - : `https://${process.env.NEXT_PUBLIC_VERCEL_BRANCH_URL}` +// Determine the appropriate base URL depending on the environment. +// 1. If on the Vercel production environment, use the production URL. +// 2. If on a Vercel preview/branch deploy, use that branch URL. +// 3. Otherwise, assume we are running locally and fall back to http://localhost:3000. + +const baseUrl = (() => { + if (process.env.NEXT_PUBLIC_VERCEL_ENV === "production") { + return `https://${process.env.NEXT_PUBLIC_VERCEL_PROJECT_PRODUCTION_URL}` + } + + if (process.env.NEXT_PUBLIC_VERCEL_BRANCH_URL) { + return `https://${process.env.NEXT_PUBLIC_VERCEL_BRANCH_URL}` + } + + // Local development fallback + return "http://localhost:3000" +})() export const siteConfig: SiteConfig = { name: "Demo Embedded Wallet", diff --git a/src/hooks/use-user.tsx b/src/hooks/use-user.tsx index 6d74c8d..e110376 100644 --- a/src/hooks/use-user.tsx +++ b/src/hooks/use-user.tsx @@ -2,47 +2,48 @@ import { useEffect, useState } from "react" import { useRouter } from "next/navigation" import { useTurnkey } from "@turnkey/sdk-react" -import { Email, User } from "@/types/turnkey" +import { UserSession } from "@/types/turnkey" +import { getSessionFromStorage } from "@/lib/storage" export const useUser = () => { - const { turnkey, client } = useTurnkey() + const { turnkey, indexedDbClient } = useTurnkey() const router = useRouter() - const [user, setUser] = useState(undefined) + const [user, setUser] = useState(undefined) useEffect(() => { const fetchUser = async () => { if (turnkey) { // Try and get the current user - const currentUser = await turnkey.getCurrentUser() + + const token = await turnkey.getSession() // If the user is not found, we assume the user is not logged in - if (!currentUser) { + if (!token?.expiry || token.expiry > Date.now()) { router.push("/") return } - // Use our read-only session to get the user's email - const client = await turnkey.currentUserSession() - - let userData: User = currentUser - // Get the user's email const { user } = - (await client?.getUser({ - organizationId: currentUser?.organization?.organizationId, - userId: currentUser?.userId, + (await indexedDbClient?.getUser({ + organizationId: token.organizationId, + userId: token.userId, })) || {} // Set the user's email in the userData object - userData = { - ...currentUser, - email: user?.userEmail as Email, - } - setUser(userData) + setUser({ + id: user?.userId || "", + name: user?.userName || "", + email: user?.userEmail || "", + organization: { + organizationId: token.organizationId, + organizationName: "", + }, + }) } } fetchUser() - }, [turnkey]) + }, [turnkey, indexedDbClient]) return { user } } diff --git a/src/lib/storage.ts b/src/lib/storage.ts new file mode 100644 index 0000000..b3b76e8 --- /dev/null +++ b/src/lib/storage.ts @@ -0,0 +1,77 @@ +import type { UserSession } from "@/types/turnkey" + +// Utility helpers for browser storage operations +// These helpers wrap the standard Web Storage API and provide typed +// convenience functions for working with the Turnkey demo app. + +// Generic helpers --------------------------------------------------------- + +/** + * Safely read and JSON-parse a value from localStorage. + * + * @param key The storage key. + * @returns Parsed value or `null` when the key doesn't exist or JSON fails. + */ +export const readLocalStorage = (key: string): T | null => { + if (typeof window === "undefined") return null + + try { + const raw = window.localStorage.getItem(key) + if (raw === null) return null + return JSON.parse(raw) as T + } catch (error) { + console.error(`[storage] Failed to read key "${key}":`, error) + return null + } +} + +/** + * Stringify and store a value in localStorage. + * + * @param key The storage key. + * @param value Any JSON-serialisable value. + */ +export const writeLocalStorage = (key: string, value: T): void => { + if (typeof window === "undefined") return + + try { + window.localStorage.setItem(key, JSON.stringify(value)) + } catch (error) { + console.error(`[storage] Failed to write key "${key}":`, error) + } +} + +/** + * Remove a key from localStorage. + */ +export const removeLocalStorage = (key: string): void => { + if (typeof window === "undefined") return + try { + window.localStorage.removeItem(key) + } catch (error) { + console.error(`[storage] Failed to remove key "${key}":`, error) + } +} + +// App-specific helpers ----------------------------------------------------- + +export const SESSION_STORAGE_KEY = "@turnkey/session/v1" +export const OTP_ID_STORAGE_KEY = "@turnkey/otpId" + +export const getSessionFromStorage = (): UserSession | null => + readLocalStorage(SESSION_STORAGE_KEY) + +export const setSessionInStorage = (session: UserSession): void => + writeLocalStorage(SESSION_STORAGE_KEY, session) + +export const removeSessionFromStorage = (): void => + removeLocalStorage(SESSION_STORAGE_KEY) + +export const getOtpIdFromStorage = (): string | null => + readLocalStorage(OTP_ID_STORAGE_KEY) + +export const setOtpIdInStorage = (otpId: string): void => + writeLocalStorage(OTP_ID_STORAGE_KEY, otpId) + +export const removeOtpIdFromStorage = (): void => + removeLocalStorage(OTP_ID_STORAGE_KEY) diff --git a/src/lib/web3.ts b/src/lib/web3.ts index b838b81..92aa217 100644 --- a/src/lib/web3.ts +++ b/src/lib/web3.ts @@ -126,7 +126,7 @@ export const watchPendingTransactions = ( } export const fundWallet = async (address: Address) => { - const fundingAmountText = "0.01 ETH" + const fundingAmountText = "1 ETH" try { const publicClient = getPublicClient() diff --git a/src/providers/auth-provider.tsx b/src/providers/auth-provider.tsx index 0d1c8d7..7dd046a 100644 --- a/src/providers/auth-provider.tsx +++ b/src/providers/auth-provider.tsx @@ -16,18 +16,23 @@ import { getSubOrgIdByPublicKey, initEmailAuth, oauth, + otpLogin, + verifyOtp, } from "@/actions/turnkey" import { googleLogout } from "@react-oauth/google" -import { - AuthClient, - ReadOnlySession, - setStorageValue, - StorageKeys, -} from "@turnkey/sdk-browser" +import { uncompressRawPublicKey } from "@turnkey/crypto" +import { AuthClient, Session, SessionType } from "@turnkey/sdk-browser" import { useTurnkey } from "@turnkey/sdk-react" import { WalletType } from "@turnkey/wallet-stamper" +import { toHex } from "viem" -import { Email, User } from "@/types/turnkey" +import { Email, UserSession } from "@/types/turnkey" +import { + getOtpIdFromStorage, + removeOtpIdFromStorage, + setOtpIdInStorage, + setSessionInStorage, +} from "@/lib/storage" export const loginResponseToUser = ( loginResponse: { @@ -39,14 +44,15 @@ export const loginResponseToUser = ( sessionExpiry?: string }, authClient: AuthClient -): User => { +): UserSession => { const subOrganization = { organizationId: loginResponse.organizationId, organizationName: loginResponse.organizationName, } - let read: ReadOnlySession | undefined + let read: Session | undefined if (loginResponse.session) { + // @ts-expect-error - Turnkey SDK types are not up to date read = { token: loginResponse.session, expiry: Number(loginResponse.sessionExpiry), @@ -54,23 +60,20 @@ export const loginResponseToUser = ( } return { - userId: loginResponse.userId, - username: loginResponse.username, + id: loginResponse.userId, + name: loginResponse.username, + email: loginResponse.username, organization: subOrganization, - session: { - read, - authClient, - }, } } type AuthActionType = - | { type: "PASSKEY"; payload: User } + | { type: "PASSKEY"; payload: UserSession } | { type: "INIT_EMAIL_AUTH" } - | { type: "COMPLETE_EMAIL_AUTH"; payload: User } - | { type: "EMAIL_RECOVERY"; payload: User } - | { type: "WALLET_AUTH"; payload: User } - | { type: "OAUTH"; payload: User } + | { type: "COMPLETE_EMAIL_AUTH"; payload: UserSession } + | { type: "EMAIL_RECOVERY"; payload: UserSession } + | { type: "WALLET_AUTH"; payload: UserSession } + | { type: "OAUTH"; payload: UserSession } | { type: "LOADING"; payload: boolean } | { type: "ERROR"; payload: string } | { type: "SESSION_EXPIRING"; payload: boolean } @@ -78,7 +81,7 @@ type AuthActionType = interface AuthState { loading: boolean error: string - user: User | null + user: UserSession | null sessionExpiring: boolean } @@ -145,19 +148,37 @@ const WARNING_BUFFER = 30 // seconds before expiry to show warning export const AuthProvider = ({ children }: { children: ReactNode }) => { const [state, dispatch] = useReducer(authReducer, initialState) const router = useRouter() - const { turnkey, authIframeClient, passkeyClient, walletClient } = - useTurnkey() + const { turnkey, indexedDbClient, passkeyClient, walletClient } = useTurnkey() const warningTimeoutRef = useRef() const initEmailLogin = async (email: Email) => { dispatch({ type: "LOADING", payload: true }) + try { + const publicKey = await indexedDbClient?.getPublicKey() + if (!publicKey) { + throw new Error("No public key found") + } + + const targetPublicKey = toHex( + uncompressRawPublicKey(new Uint8Array(Buffer.from(publicKey, "hex"))) + ) + + if (!targetPublicKey) { + throw new Error("No public key found") + } + const response = await initEmailAuth({ email, - targetPublicKey: `${authIframeClient?.iframePublicKey}`, + targetPublicKey, + baseUrl: window.location.origin, }) if (response) { + // Persist otpId locally so it can be reused after page reloads + if (response.otpId) { + setOtpIdInStorage(response.otpId) + } dispatch({ type: "INIT_EMAIL_AUTH" }) router.push(`/email-auth?userEmail=${encodeURIComponent(email)}`) } @@ -177,25 +198,58 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => { continueWith: string credentialBundle: string }) => { + // validate inputs and begin auth flow + if (userEmail && continueWith === "email" && credentialBundle) { dispatch({ type: "LOADING", payload: true }) try { - await authIframeClient?.injectCredentialBundle(credentialBundle) - if (authIframeClient?.iframePublicKey) { - const loginResponse = - await authIframeClient?.loginWithReadWriteSession( - authIframeClient.iframePublicKey, - SESSION_EXPIRY - ) - if (loginResponse?.organizationId) { - // Schedule warning for session expiry - const expiryTime = Date.now() + parseInt(SESSION_EXPIRY) * 1000 - scheduleSessionWarning(expiryTime) - router.push("/dashboard") - } + const publicKeyCompressed = await indexedDbClient?.getPublicKey() + if (!publicKeyCompressed) { + throw new Error("No public key found") } + // We keep the compressed key form for downstream calls + + // Retrieve persisted otpId + const storedOtpId = getOtpIdFromStorage() + + if (!storedOtpId) { + throw new Error("OTP identifier not found. Please restart sign-in.") + } + const authResponse = await verifyOtp({ + otpId: storedOtpId, + publicKey: publicKeyCompressed, + otpCode: credentialBundle, + }) + + const { session, userId, organizationId } = await otpLogin({ + email: userEmail as Email, + publicKey: publicKeyCompressed, + verificationToken: authResponse.verificationToken, + }) + + await indexedDbClient?.loginWithSession(session || "") + + // Clear persisted otpId after successful login + removeOtpIdFromStorage() + + // Schedule warning for session expiry + const expiryTime = Date.now() + parseInt(SESSION_EXPIRY) * 1000 + scheduleSessionWarning(expiryTime) + + setSessionInStorage({ + id: userId, + name: userEmail, + email: userEmail, + organization: { + organizationId: organizationId, + organizationName: "", + }, + }) + + router.push("/dashboard") } catch (error: any) { + console.error("[completeEmailAuth] Error:", error) dispatch({ type: "ERROR", payload: error.message }) } finally { dispatch({ type: "LOADING", payload: false }) @@ -209,14 +263,14 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => { const subOrgId = await getSubOrgIdByEmail(email as Email) if (subOrgId?.length) { - const loginResponse = await passkeyClient?.login() - if (loginResponse?.organizationId) { - dispatch({ - type: "PASSKEY", - payload: loginResponseToUser(loginResponse, AuthClient.Passkey), - }) - router.push("/dashboard") - } + await indexedDbClient?.resetKeyPair() + const publicKey = await indexedDbClient!.getPublicKey() + await passkeyClient?.loginWithPasskey({ + sessionType: SessionType.READ_WRITE, + publicKey, + }) + + router.push("/dashboard") } else { // User either does not have an account with a sub organization // or does not have a passkey @@ -242,8 +296,8 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => { }) if (subOrg && user) { - await setStorageValue( - StorageKeys.UserSession, + // Store session using browser localStorage + setSessionInStorage( loginResponseToUser( { userId: user.userId, @@ -272,52 +326,36 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => { dispatch({ type: "LOADING", payload: true }) try { - const publicKey = await walletClient?.getPublicKey() + await indexedDbClient?.resetKeyPair() + const publicKey = await indexedDbClient?.getPublicKey() + const walletPublicKey = await walletClient?.getPublicKey() - if (!publicKey) { + if (!publicKey || !walletPublicKey) { throw new Error("No public key found") } // Try and get the suborg id given the user's wallet public key - const subOrgId = await getSubOrgIdByPublicKey(publicKey) - + let subOrgId = await getSubOrgIdByPublicKey(walletPublicKey) + let user = null // If the user has a suborg id, use the oauth flow to login - if (subOrgId) { - const loginResponse = await walletClient?.login({ - organizationId: subOrgId, - }) - - if (loginResponse?.organizationId) { - router.push("/dashboard") - } - } else { + if (!subOrgId) { // If the user does not have a suborg id, create a new suborg for the user - const { subOrg, user } = await createUserSubOrg({ + const { subOrg, user: newUser } = await createUserSubOrg({ wallet: { - publicKey: publicKey, + publicKey: walletPublicKey, type: WalletType.Ethereum, }, }) + subOrgId = subOrg.subOrganizationId + user = newUser + } - if (subOrg && user) { - await setStorageValue( - StorageKeys.UserSession, - loginResponseToUser( - { - userId: user.userId, - username: user.userName, - organizationId: subOrg.subOrganizationId, - organizationName: "", - session: undefined, - sessionExpiry: undefined, - }, - AuthClient.Wallet - ) - ) + await walletClient?.loginWithWallet({ + publicKey, + sessionType: SessionType.READ_WRITE, + }) - router.push("/dashboard") - } - } + router.push("/dashboard") } catch (error: any) { dispatch({ type: "ERROR", payload: error.message }) } finally { @@ -328,6 +366,18 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => { const loginWithOAuth = async (credential: string, providerName: string) => { dispatch({ type: "LOADING", payload: true }) try { + const publicKeyCompressed = await indexedDbClient?.getPublicKey() + + if (!publicKeyCompressed) { + throw new Error("No public key found") + } + + const publicKey = toHex( + uncompressRawPublicKey( + new Uint8Array(Buffer.from(publicKeyCompressed, "hex")) + ) + ).replace("0x", "") + // Determine if the user has a sub-organization associated with their email let subOrgId = await getSubOrgId({ oidcToken: credential }) @@ -343,26 +393,17 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => { subOrgId = subOrg.subOrganizationId } - if (authIframeClient?.iframePublicKey) { - const oauthResponse = await oauth({ - credential, - targetPublicKey: authIframeClient?.iframePublicKey, - targetSubOrgId: subOrgId, - }) - const injectSuccess = await authIframeClient?.injectCredentialBundle( - oauthResponse.credentialBundle - ) - if (injectSuccess) { - const loginResponse = - await authIframeClient?.loginWithReadWriteSession( - authIframeClient.iframePublicKey, - SESSION_EXPIRY - ) - if (loginResponse?.organizationId) { - router.push("/dashboard") - } - } - } + // Note: You need to use the compressed public key here because when the + // indexedDbClient stamps the request and the key is compared in the backend it expects the compressed version + const oauthResponse = await oauth({ + credential, + publicKey: publicKeyCompressed, + subOrgId, + }) + + await indexedDbClient?.loginWithSession(oauthResponse.session) + + router.push("/dashboard") } catch (error: any) { dispatch({ type: "ERROR", payload: error.message }) } finally { @@ -383,7 +424,8 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => { } const logout = async () => { - await turnkey?.logoutUser() + await turnkey?.logout() + await indexedDbClient?.clear() googleLogout() router.push("/") } diff --git a/src/providers/index.tsx b/src/providers/index.tsx index 471e890..d9bbd6c 100644 --- a/src/providers/index.tsx +++ b/src/providers/index.tsx @@ -10,6 +10,12 @@ import { ThemeProvider } from "./theme-provider" const wallet = new EthereumWallet() +// Helper to get current hostname in runtime; falls back to configured value during SSR. +const getRuntimeRpId = () => + typeof window !== "undefined" + ? window.location.hostname + : turnkeyConfig.passkey.rpId + export const Providers: React.FC<{ children: React.ReactNode }> = ({ children, }) => ( @@ -22,7 +28,7 @@ export const Providers: React.FC<{ children: React.ReactNode }> = ({ > { const { wallets } = await browserClient.getWallets() @@ -148,7 +149,7 @@ async function getWalletsWithAccounts( // the wallet name export function WalletsProvider({ children }: { children: ReactNode }) { const [state, dispatch] = useReducer(walletsReducer, initialState) - const { client, turnkey } = useTurnkey() + const { indexedDbClient } = useTurnkey() const { user } = useUser() const [preferredWallet, setPreferredWallet] = useLocalStorage(PREFERRED_WALLET_KEY, { @@ -166,10 +167,9 @@ export function WalletsProvider({ children }: { children: ReactNode }) { dispatch({ type: "SET_LOADING", payload: true }) try { // We'll try and get the current user's read-only session - const browserClient = await turnkey?.currentUserSession() - if (browserClient) { + if (indexedDbClient) { const wallets = await getWalletsWithAccounts( - browserClient, + indexedDbClient, user?.organization?.organizationId ) dispatch({ type: "SET_WALLETS", payload: wallets }) @@ -180,7 +180,7 @@ export function WalletsProvider({ children }: { children: ReactNode }) { const wallet = wallets.find( (wallet) => wallet.walletId === preferredWallet.walletId && - user?.userId === preferredWallet.userId + user?.id === preferredWallet.userId ) // Preferred wallet is found select it as the current wallet @@ -192,13 +192,12 @@ export function WalletsProvider({ children }: { children: ReactNode }) { selectWallet(selectedWallet) } } else { - const currentUser = await turnkey?.getCurrentUser() - if (currentUser?.organization.organizationId) { + if (user?.organization.organizationId) { // This case occurs when the user signs up with a new passkey; since a read-only session is not created for new passkey sign-ups, // we need to fetch the wallets from the server const wallets = await server.getWalletsWithAccounts( - currentUser?.organization.organizationId + user?.organization.organizationId ) dispatch({ type: "SET_WALLETS", payload: wallets }) if (wallets.length > 0) { @@ -213,7 +212,7 @@ export function WalletsProvider({ children }: { children: ReactNode }) { } } fetchWallets() - }, [user, turnkey]) + }, [user, indexedDbClient]) useEffect(() => { if (state.selectedWallet) { @@ -224,11 +223,11 @@ export function WalletsProvider({ children }: { children: ReactNode }) { const newWalletAccount = async () => { dispatch({ type: "SET_LOADING", payload: true }) try { - if (state.selectedWallet && client) { + if (state.selectedWallet && indexedDbClient) { const newAccount = defaultEthereumAccountAtIndex( state.selectedWallet.accounts.length + 1 ) - const response = await client.createWalletAccounts({ + const response = await indexedDbClient.createWalletAccounts({ walletId: state.selectedWallet.walletId, accounts: [newAccount], }) @@ -267,8 +266,8 @@ export function WalletsProvider({ children }: { children: ReactNode }) { const newWallet = async (walletName?: string) => { dispatch({ type: "SET_LOADING", payload: true }) try { - if (client) { - const { walletId } = await client.createWallet({ + if (indexedDbClient) { + const { walletId } = await indexedDbClient.createWallet({ walletName: walletName || "New Wallet", accounts: DEFAULT_ETHEREUM_ACCOUNTS, }) @@ -290,7 +289,7 @@ export function WalletsProvider({ children }: { children: ReactNode }) { const selectWallet = (wallet: Wallet) => { dispatch({ type: "SET_SELECTED_WALLET", payload: wallet }) setPreferredWallet({ - userId: user?.userId || "", + userId: user?.id || "", walletId: wallet.walletId, }) } diff --git a/src/types/turnkey.ts b/src/types/turnkey.ts index a430937..5736845 100644 --- a/src/types/turnkey.ts +++ b/src/types/turnkey.ts @@ -1,5 +1,4 @@ -import { TurnkeyApiTypes, type TurnkeyClient } from "@turnkey/http" -import { Turnkey } from "@turnkey/sdk-browser" +import { TurnkeyApiTypes } from "@turnkey/http" import { Address } from "viem" export type Attestation = TurnkeyApiTypes["v1Attestation"] @@ -18,9 +17,14 @@ export type Wallet = accounts: Account[] } -export type User = Awaited> & { - email?: Email - readOnlySession?: ReadOnlySession +export type UserSession = { + id: string + name: string + email: string + organization: { + organizationId: string + organizationName: string + } } export type Authenticator =