From 0891ec34effb4471f9ac0cde1b1f27024401bc58 Mon Sep 17 00:00:00 2001 From: Tony Giorgio Date: Tue, 28 Oct 2025 13:13:29 -0500 Subject: [PATCH 1/4] Add anonymous/guest authentication with Bitcoin-only payment enforcement Implements guest user authentication system with strict requirements: - Guest signup/login without email address using UUID + password - Bitcoin-only yearly payment enforcement for guest accounts - Multi-step signup flow with comprehensive warnings - Payment blocking until guest users subscribe - Guest account badge and limitations display New Components: - GuestSignupWarningDialog: Multi-checkbox warnings before signup (Bitcoin-only, no support, no free trial, backup credentials) - GuestCredentialsDialog: One-time display of Account UUID with backup instructions - GuestPaymentWarningDialog: Non-dismissible modal blocking chat access for unpaid guests - checkbox.tsx: Radix UI checkbox component for warning confirmations Modified Components: - signup.tsx: Added guest signup flow with warning and credentials dialogs - login.tsx: Added guest login with UUID + password fields - index.tsx: Added payment warning detection for unpaid guest users - pricing.tsx: Enforced Bitcoin-only toggle for guest accounts (disabled, auto-enabled) - AccountDialog.tsx: Added "Anonymous Account" badge and limitations info Key Features: - SDK integration with signUpGuest() and signInGuest() methods - Case-insensitive login_method detection ("guest") - Payment warning excluded on pricing page to allow subscription - Non-dismissible payment dialog (prevents ESC/outside clicks) - Mobile-friendly Account UUID display without auto-select Dependencies: - Added @radix-ui/react-checkbox@1.3.3 Fixes #287 Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> Fix guest user payment flows and simplify account dialog - Allow guest users to pass empty email (backend infers from JWT token) - Disable "Pay with Card" for guest users in API credits purchase - Enable Bitcoin payments for guest users - Simplify AccountDialog: only hide email field for guests - Remove unnecessary "Anonymous Account" badge and limitations box This allows guest users to: - Purchase subscriptions on pricing page with Bitcoin - Purchase API credits with Bitcoin only - Have a clean account dialog experience Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> Address PR feedback: improve type safety and prevent credentials dialog dismissal - Add LoginResponse type from @opensecret/react for strict TypeScript compliance - Make GuestCredentialsDialog non-dismissible (prevent ESC/outside click) - Forces users to explicitly confirm they've saved their Account ID before continuing This ensures users cannot accidentally bypass saving their credentials. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> Address additional PR feedback 1. Fix Radix Checkbox type annotations - Remove incorrect 'boolean' type annotation from onCheckedChange handlers - Let TypeScript infer correct type: boolean | 'indeterminate' - Applied to all three checkboxes in GuestSignupWarningDialog 2. Fix guest user checkout flow in pricing page - Reuse isGuestUser variable instead of redefining - Skip email verification check for guest users - Add isGuestUser to useCallback dependencies to prevent stale closures 3. Fix terminology consistency - Changed 'Account UUID' to 'Account ID' in signup password hint - Matches terminology used throughout the rest of the UI Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> --- frontend/bun.lock | 65 ++++++-- frontend/package.json | 1 + frontend/src/components/AccountDialog.tsx | 73 ++++----- .../src/components/GuestCredentialsDialog.tsx | 147 ++++++++++++++++++ .../components/GuestPaymentWarningDialog.tsx | 72 +++++++++ .../components/GuestSignupWarningDialog.tsx | 136 ++++++++++++++++ .../components/apikeys/ApiCreditsSection.tsx | 15 +- frontend/src/components/ui/checkbox.tsx | 26 ++++ frontend/src/routes/index.tsx | 23 ++- frontend/src/routes/login.tsx | 103 +++++++++++- frontend/src/routes/pricing.tsx | 27 +++- frontend/src/routes/signup.tsx | 119 +++++++++++++- 12 files changed, 730 insertions(+), 77 deletions(-) create mode 100644 frontend/src/components/GuestCredentialsDialog.tsx create mode 100644 frontend/src/components/GuestPaymentWarningDialog.tsx create mode 100644 frontend/src/components/GuestSignupWarningDialog.tsx create mode 100644 frontend/src/components/ui/checkbox.tsx diff --git a/frontend/bun.lock b/frontend/bun.lock index 5a3660e95..ce74e93cc 100644 --- a/frontend/bun.lock +++ b/frontend/bun.lock @@ -7,6 +7,7 @@ "@opensecret/react": "1.5.0", "@radix-ui/react-alert-dialog": "^1.1.1", "@radix-ui/react-avatar": "^1.1.0", + "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-dialog": "^1.1.1", "@radix-ui/react-dropdown-menu": "^2.1.1", "@radix-ui/react-label": "^2.1.0", @@ -257,6 +258,8 @@ "@radix-ui/react-avatar": ["@radix-ui/react-avatar@1.1.3", "", { "dependencies": { "@radix-ui/react-context": "1.1.1", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Paen00T4P8L8gd9bNsRMw7Cbaz85oxiv+hzomsRZgFm2byltPFDtfcoqlWJ8GyZlIBWgLssJlzLCnKU0G0302g=="], + "@radix-ui/react-checkbox": ["@radix-ui/react-checkbox@1.3.3", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-use-size": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw=="], + "@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.2", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-slot": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9z54IEKRxIa9VityapoEYMuByaG42iSy1ZXlY2KcuLSEtq8x4987/N6m15ppoMffgZX72gER2uHe1D9Y6Unlcw=="], "@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw=="], @@ -285,7 +288,7 @@ "@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.4", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA=="], - "@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.2", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg=="], + "@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ=="], "@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.0.2", "", { "dependencies": { "@radix-ui/react-slot": "1.1.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w=="], @@ -305,7 +308,7 @@ "@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw=="], - "@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.1.0", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw=="], + "@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="], "@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA=="], @@ -313,11 +316,11 @@ "@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w=="], - "@radix-ui/react-use-previous": ["@radix-ui/react-use-previous@1.1.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og=="], + "@radix-ui/react-use-previous": ["@radix-ui/react-use-previous@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ=="], "@radix-ui/react-use-rect": ["@radix-ui/react-use-rect@1.1.0", "", { "dependencies": { "@radix-ui/rect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ=="], - "@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.1.0", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw=="], + "@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ=="], "@radix-ui/react-visually-hidden": ["@radix-ui/react-visually-hidden@1.1.2", "", { "dependencies": { "@radix-ui/react-primitive": "2.0.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-1SzA4ns2M1aRlvxErqhLHsBHoS5eI5UUcI2awAMgGUp4LoaoWOKYmvqDY2s/tltuPkh3Yk77YF/r3IRj+Amx4Q=="], @@ -1173,8 +1176,30 @@ "@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="], + "@radix-ui/react-checkbox/@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="], + + "@radix-ui/react-checkbox/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="], + + "@radix-ui/react-checkbox/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="], + + "@radix-ui/react-checkbox/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], + + "@radix-ui/react-dialog/@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.2", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg=="], + + "@radix-ui/react-dialog/@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.1.0", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw=="], + + "@radix-ui/react-dropdown-menu/@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.1.0", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw=="], + + "@radix-ui/react-menu/@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.2", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg=="], + "@radix-ui/react-menu/@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.2", "", { "dependencies": { "@radix-ui/primitive": "1.1.1", "@radix-ui/react-collection": "1.1.2", "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-context": "1.1.1", "@radix-ui/react-direction": "1.1.0", "@radix-ui/react-id": "1.1.0", "@radix-ui/react-primitive": "2.0.2", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-controllable-state": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-zgMQWkNO169GtGqRvYrzb0Zf8NhMHS2DuEB/TiEmVnpr5OqPU3i8lfbxaAmC2J/KYuIQxyoQQ6DxepyXp61/xw=="], + "@radix-ui/react-popper/@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.1.0", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw=="], + + "@radix-ui/react-presence/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="], + + "@radix-ui/react-presence/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="], + "@radix-ui/react-roving-focus/@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="], "@radix-ui/react-roving-focus/@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw=="], @@ -1191,7 +1216,15 @@ "@radix-ui/react-roving-focus/@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="], - "@radix-ui/react-roving-focus/@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="], + "@radix-ui/react-select/@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.1.0", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw=="], + + "@radix-ui/react-select/@radix-ui/react-use-previous": ["@radix-ui/react-use-previous@1.1.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og=="], + + "@radix-ui/react-switch/@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.1.0", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw=="], + + "@radix-ui/react-switch/@radix-ui/react-use-previous": ["@radix-ui/react-use-previous@1.1.0", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og=="], + + "@radix-ui/react-switch/@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.1.0", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw=="], "@radix-ui/react-tabs/@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="], @@ -1201,14 +1234,18 @@ "@radix-ui/react-tabs/@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="], - "@radix-ui/react-tabs/@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ=="], - "@radix-ui/react-tabs/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="], - "@radix-ui/react-tabs/@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="], + "@radix-ui/react-tooltip/@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.2", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.0" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg=="], + + "@radix-ui/react-tooltip/@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.1.0", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw=="], + + "@radix-ui/react-use-controllable-state/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="], "@radix-ui/react-use-effect-event/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="], + "@radix-ui/react-use-size/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="], + "@tauri-apps/plugin-opener/@tauri-apps/api": ["@tauri-apps/api@2.4.1", "", {}, "sha512-5sYwZCSJb6PBGbBL4kt7CnE5HHbBqwH+ovmOW6ZVju3nX4E3JX6tt2kRklFEH7xMOIwR0btRkZktuLhKvyEQYg=="], "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], @@ -1245,24 +1282,20 @@ "wrap-ansi-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "@radix-ui/react-checkbox/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], + + "@radix-ui/react-menu/@radix-ui/react-roving-focus/@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.1.0", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw=="], + "@radix-ui/react-roving-focus/@radix-ui/react-collection/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], "@radix-ui/react-roving-focus/@radix-ui/react-id/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="], "@radix-ui/react-roving-focus/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - "@radix-ui/react-roving-focus/@radix-ui/react-use-controllable-state/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="], - "@radix-ui/react-tabs/@radix-ui/react-id/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="], - "@radix-ui/react-tabs/@radix-ui/react-presence/@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="], - - "@radix-ui/react-tabs/@radix-ui/react-presence/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="], - "@radix-ui/react-tabs/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - "@radix-ui/react-tabs/@radix-ui/react-use-controllable-state/@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="], - "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], "glob/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], diff --git a/frontend/package.json b/frontend/package.json index e94a3c95f..37b01f835 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -20,6 +20,7 @@ "@opensecret/react": "1.5.0", "@radix-ui/react-alert-dialog": "^1.1.1", "@radix-ui/react-avatar": "^1.1.0", + "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-dialog": "^1.1.1", "@radix-ui/react-dropdown-menu": "^2.1.1", "@radix-ui/react-label": "^2.1.0", diff --git a/frontend/src/components/AccountDialog.tsx b/frontend/src/components/AccountDialog.tsx index a2b41e12a..5d3202a3a 100644 --- a/frontend/src/components/AccountDialog.tsx +++ b/frontend/src/components/AccountDialog.tsx @@ -36,8 +36,9 @@ export function AccountDialog() { ); const { billingStatus } = useLocalState(); - // Check if user is an email user + // Check user login method const isEmailUser = os.auth.user?.user.login_method === "email"; + const isGuestUser = os.auth.user?.user.login_method?.toLowerCase() === "guest"; const handleResendVerification = async () => { try { @@ -56,41 +57,43 @@ export function AccountDialog() { Change your email or upgrade your plan.
-
- -
- - {os.auth.user?.user.email_verified ? ( - - ) : ( - - )} -
- {!os.auth.user?.user.email_verified && ( -
- {verificationStatus === "unverified" ? ( - <> - Unverified -{" "} - - + {!isGuestUser && ( +
+ +
+ + {os.auth.user?.user.email_verified ? ( + ) : ( - "Pending - Check your email for verification link" + )}
- )} -
+ {!os.auth.user?.user.email_verified && ( +
+ {verificationStatus === "unverified" ? ( + <> + Unverified -{" "} + + + ) : ( + "Pending - Check your email for verification link" + )} +
+ )} +
+ )}
+ +
+

+ You will need this Account ID along with your password to sign in. +

+
+ + {/* Backup Instructions */} +
+

How to save your Account ID:

+
    +
  • Copy it to a password manager (recommended)
  • +
  • Write it down on paper and store it safely
  • +
  • Save it in a secure note or encrypted file
  • +
  • Take a screenshot and store it securely
  • +
+
+ + {/* Confirmation Checkbox */} +
+ setHasBackedUp(checked === true)} + /> + +
+ + + + + + + + ); +} diff --git a/frontend/src/components/GuestPaymentWarningDialog.tsx b/frontend/src/components/GuestPaymentWarningDialog.tsx new file mode 100644 index 000000000..9fabcfa6c --- /dev/null +++ b/frontend/src/components/GuestPaymentWarningDialog.tsx @@ -0,0 +1,72 @@ +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle +} from "@/components/ui/dialog"; +import { useNavigate } from "@tanstack/react-router"; +import { useOpenSecret } from "@opensecret/react"; +import { AlertTriangle, LogOut, CreditCard } from "lucide-react"; + +interface GuestPaymentWarningDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; +} + +export function GuestPaymentWarningDialog({ open, onOpenChange }: GuestPaymentWarningDialogProps) { + const navigate = useNavigate(); + const os = useOpenSecret(); + + const handleGoToPricing = () => { + navigate({ to: "/pricing" }); + }; + + const handleLogout = async () => { + await os.signOut(); + }; + + return ( + + e.preventDefault()} + onEscapeKeyDown={(e) => e.preventDefault()} + > + + + + Subscription Required + + + Anonymous accounts require a paid subscription to use Maple AI. + + + +
+
+

+ Your anonymous account is currently on the Free plan and cannot use the chat feature. +

+

+ To start chatting with Maple AI, you need to subscribe to a paid plan. Anonymous + accounts must pay for a full year using Bitcoin. +

+
+ +
+ + +
+
+
+
+ ); +} diff --git a/frontend/src/components/GuestSignupWarningDialog.tsx b/frontend/src/components/GuestSignupWarningDialog.tsx new file mode 100644 index 000000000..d1bb20f8d --- /dev/null +++ b/frontend/src/components/GuestSignupWarningDialog.tsx @@ -0,0 +1,136 @@ +import { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle +} from "@/components/ui/dialog"; +import { AlertTriangle } from "lucide-react"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Label } from "@/components/ui/label"; + +interface GuestSignupWarningDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + onAccept: () => void; +} + +export function GuestSignupWarningDialog({ + open, + onOpenChange, + onAccept +}: GuestSignupWarningDialogProps) { + const [bitcoinPaymentAgreed, setBitcoinPaymentAgreed] = useState(false); + const [noSupportAgreed, setNoSupportAgreed] = useState(false); + const [backupCredentialsAgreed, setBackupCredentialsAgreed] = useState(false); + + const allAgreed = bitcoinPaymentAgreed && noSupportAgreed && backupCredentialsAgreed; + + const handleAccept = () => { + if (allAgreed) { + // Reset checkboxes for next time + setBitcoinPaymentAgreed(false); + setNoSupportAgreed(false); + setBackupCredentialsAgreed(false); + onAccept(); + } + }; + + return ( + + + + + + Anonymous Account - Important Warnings + + + Please read and acknowledge all warnings before creating an anonymous account. + + + +
+
+ {/* Warning 1: Bitcoin Payment */} +
+ setBitcoinPaymentAgreed(checked === true)} + /> +
+ +
+
+ + {/* Warning 2: No Support */} +
+ setNoSupportAgreed(checked === true)} + /> +
+ +
+
+ + {/* Warning 3: Backup Credentials */} +
+ setBackupCredentialsAgreed(checked === true)} + /> +
+ +
+
+
+ +
+

+ Why these restrictions? +

+

+ Anonymous accounts are designed for users who prioritize privacy and don't want to + provide an email address. This comes with trade-offs in support and payment options. +

+
+
+ + + + + +
+
+ ); +} diff --git a/frontend/src/components/apikeys/ApiCreditsSection.tsx b/frontend/src/components/apikeys/ApiCreditsSection.tsx index a510a859d..a5438931a 100644 --- a/frontend/src/components/apikeys/ApiCreditsSection.tsx +++ b/frontend/src/components/apikeys/ApiCreditsSection.tsx @@ -43,6 +43,7 @@ export function ApiCreditsSection({ showSuccessMessage = false }: ApiCreditsSect const isMobilePlatform = isMobile(); const userEmail = auth.user?.user.email; + const isGuestUser = auth.user?.user.login_method?.toLowerCase() === "guest"; useEffect(() => { if (showSuccessMessage) { @@ -124,16 +125,10 @@ export function ApiCreditsSection({ showSuccessMessage = false }: ApiCreditsSect // For mobile platforms, we would need special handling with invoke("plugin:opener|open_url"), but that's not needed here. window.location.href = response.checkout_url; } else { - // For Zaprite, we need the user's email - if (!userEmail) { - setPurchaseError("Email is required for Bitcoin payments"); - setIsPurchasing(false); - setPaymentMethod(null); - return; - } + // For Zaprite, guest users pass empty string (backend infers from JWT) const response = await billingService.purchaseApiCreditsZaprite({ credits: finalPackage.credits, - email: userEmail, + email: userEmail || "", success_url: successUrl }); @@ -294,11 +289,13 @@ export function ApiCreditsSection({ showSuccessMessage = false }: ApiCreditsSect onClick={() => handlePurchase("stripe")} disabled={ isPurchasing || + isGuestUser || (showCustomAmount && (!customAmount || parseInt(customAmount) < 10 || parseInt(customAmount) > 1000)) } className="flex-1 w-full sm:w-auto" size="default" + title={isGuestUser ? "Anonymous accounts must use Bitcoin" : undefined} > {isPurchasing && paymentMethod === "stripe" ? ( @@ -312,14 +309,12 @@ export function ApiCreditsSection({ showSuccessMessage = false }: ApiCreditsSect onClick={() => handlePurchase("zaprite")} disabled={ isPurchasing || - !userEmail || (showCustomAmount && (!customAmount || parseInt(customAmount) < 10 || parseInt(customAmount) > 1000)) } variant="outline" className="flex-1 w-full sm:w-auto" size="default" - title={!userEmail ? "Email required for Bitcoin payments" : undefined} > {isPurchasing && paymentMethod === "zaprite" ? ( diff --git a/frontend/src/components/ui/checkbox.tsx b/frontend/src/components/ui/checkbox.tsx new file mode 100644 index 000000000..16030ae21 --- /dev/null +++ b/frontend/src/components/ui/checkbox.tsx @@ -0,0 +1,26 @@ +import * as React from "react"; +import * as CheckboxPrimitive from "@radix-ui/react-checkbox"; +import { Check } from "lucide-react"; + +import { cn } from "@/utils/utils"; + +const Checkbox = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + + +)); +Checkbox.displayName = CheckboxPrimitive.Root.displayName; + +export { Checkbox }; diff --git a/frontend/src/routes/index.tsx b/frontend/src/routes/index.tsx index fb358a301..6a0e1394b 100644 --- a/frontend/src/routes/index.tsx +++ b/frontend/src/routes/index.tsx @@ -4,6 +4,7 @@ import { UnifiedChat } from "@/components/UnifiedChat"; import { Marketing } from "@/components/Marketing"; import { TopNav } from "@/components/TopNav"; import { VerificationModal } from "@/components/VerificationModal"; +import { GuestPaymentWarningDialog } from "@/components/GuestPaymentWarningDialog"; import { TeamManagementDialog } from "@/components/team/TeamManagementDialog"; import { ApiKeyManagementDialog } from "@/components/apikeys/ApiKeyManagementDialog"; import { useOpenSecret } from "@opensecret/react"; @@ -38,7 +39,7 @@ function Index() { const navigate = useNavigate(); const os = useOpenSecret(); const queryClient = useQueryClient(); - const { setBillingStatus } = useLocalState(); + const { setBillingStatus, billingStatus } = useLocalState(); const { login, next, team_setup, credits_success } = Route.useSearch(); @@ -46,6 +47,7 @@ function Index() { const [teamDialogOpen, setTeamDialogOpen] = useState(false); const [apiKeyDialogOpen, setApiKeyDialogOpen] = useState(false); const [showCreditSuccess, setShowCreditSuccess] = useState(false); + const [showGuestPaymentWarning, setShowGuestPaymentWarning] = useState(false); // Proactively fetch billing status for authenticated users useQuery({ @@ -103,6 +105,21 @@ function Index() { } }, [credits_success, os.auth.user, navigate, queryClient]); + // Check if guest user needs to pay + const isGuestUser = os.auth.user?.user.login_method?.toLowerCase() === "guest"; + const isOnFreePlan = billingStatus?.product_name?.toLowerCase().includes("free") ?? false; + const shouldShowGuestPaymentWarning = isGuestUser && isOnFreePlan && billingStatus !== null; + + // Show guest payment warning if needed (but not on pricing page) + useEffect(() => { + const currentPath = window.location.pathname; + if (shouldShowGuestPaymentWarning && currentPath !== "/pricing") { + setShowGuestPaymentWarning(true); + } else { + setShowGuestPaymentWarning(false); + } + }, [shouldShowGuestPaymentWarning]); + // Show marketing page for non-authenticated users if (!os.auth.user) { return ( @@ -121,6 +138,10 @@ function Index() { {/* Modals */} + {/* Team Management Dialog */} ) => { + e.preventDefault(); + setIsLoading(true); + setError(null); + const formData = new FormData(e.currentTarget); + const uuid = formData.get("uuid") as string; + const password = formData.get("password") as string; + + try { + await os.signInGuest(uuid, password); + // Clear any existing billing token to prevent session mixing + try { + getBillingService().clearToken(); + } catch (billingError) { + console.warn("Failed to clear billing token:", billingError); + } + setTimeout(() => { + if (selected_plan) { + navigate({ + to: "/pricing", + search: { selected_plan } + }); + } else { + navigate({ to: next || "/" }); + } + window.scrollTo(0, 0); + }, 100); + } catch (error) { + console.error(error); + if (error instanceof Error) { + setError(`${error.message}`); + } else { + setError("Something went wrong. Please try again."); + } + } finally { + setIsLoading(false); + } + }; + const handleAppleLogin = async () => { try { if (isTauriEnv && isIOSPlatform) { @@ -306,6 +345,10 @@ function LoginPage() { inviteCode="" /> )} +
Need an account?{" "} @@ -316,6 +359,62 @@ function LoginPage() { ); } + if (loginMethod === "guest") { + return ( + + + {error && } +
+ + +
+
+ + +
+ + +
+ Need an account?{" "} + + Sign up + +
+
+ + ); + } + return (
diff --git a/frontend/src/routes/pricing.tsx b/frontend/src/routes/pricing.tsx index 1ce9f93ac..b942b13ff 100644 --- a/frontend/src/routes/pricing.tsx +++ b/frontend/src/routes/pricing.tsx @@ -188,6 +188,7 @@ function PricingPage() { const os = useOpenSecret(); const { setBillingStatus } = useLocalState(); const isLoggedIn = !!os.auth.user; + const isGuestUser = os.auth.user?.user.login_method?.toLowerCase() === "guest"; const { selected_plan } = Route.useSearch(); // Use platform detection functions @@ -207,12 +208,12 @@ function PricingPage() { enabled: isLoggedIn }); - // Auto-enable Bitcoin toggle for Zaprite users (except on mobile platforms) + // Auto-enable Bitcoin toggle for Zaprite users (except on mobile platforms) and guest users useEffect(() => { - if (freshBillingStatus?.payment_provider === "zaprite" && !isMobilePlatform) { + if ((freshBillingStatus?.payment_provider === "zaprite" && !isMobilePlatform) || isGuestUser) { setUseBitcoin(true); } - }, [freshBillingStatus?.payment_provider, isMobilePlatform]); + }, [freshBillingStatus?.payment_provider, isMobilePlatform, isGuestUser]); // Always try to fetch portal URL if logged in const { data: portalUrl } = useQuery({ @@ -347,8 +348,8 @@ function PricingPage() { return; } - // Check if email is verified before proceeding to checkout (skip in dev mode) - if (!import.meta.env.DEV && !os.auth.user?.user.email_verified) { + // Check if email is verified before proceeding (skip for guests and in dev mode) + if (!import.meta.env.DEV && !isGuestUser && !os.auth.user?.user.email_verified) { console.log("Email verification required before checkout"); return; } @@ -356,8 +357,11 @@ function PricingPage() { setLoadingProductId(productId); try { const billingService = getBillingService(); - const email = os.auth.user?.user.email; - if (!email) { + + // For guest users, pass empty string (backend infers user from JWT token) + const email = isGuestUser ? "" : os.auth.user?.user.email || ""; + + if (!email && !isGuestUser) { throw new Error("User email not found"); } @@ -410,6 +414,7 @@ function PricingPage() { navigate, os.auth.user?.user.email, os.auth.user?.user.email_verified, + isGuestUser, useBitcoin, isMobilePlatform ] @@ -892,7 +897,7 @@ function PricingPage() { ); })()} -
+
@@ -902,9 +907,15 @@ function PricingPage() { id="bitcoin-toggle" checked={useBitcoin} onCheckedChange={setUseBitcoin} + disabled={isGuestUser} className="data-[state=checked]:bg-[hsl(var(--bitcoin))] data-[state=unchecked]:border-foreground/30 scale-100" />
+ {isGuestUser && ( +
+ Anonymous accounts must pay with Bitcoin (yearly only) +
+ )}
diff --git a/frontend/src/routes/signup.tsx b/frontend/src/routes/signup.tsx index f2e404a9f..cde70954c 100644 --- a/frontend/src/routes/signup.tsx +++ b/frontend/src/routes/signup.tsx @@ -1,5 +1,5 @@ import { createFileRoute, Link, useNavigate } from "@tanstack/react-router"; -import { useOpenSecret } from "@opensecret/react"; +import { useOpenSecret, type LoginResponse } from "@opensecret/react"; import { useEffect, useState } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; @@ -17,6 +17,9 @@ import { bytesToHex } from "@noble/hashes/utils"; import { AppleAuthProvider } from "@/components/AppleAuthProvider"; import { getBillingService } from "@/billing/billingService"; import { isIOS, isTauri } from "@/utils/platform"; +import { GuestSignupWarningDialog } from "@/components/GuestSignupWarningDialog"; +import { GuestCredentialsDialog } from "@/components/GuestCredentialsDialog"; +import { UserCircle } from "lucide-react"; type SignupSearchParams = { next?: string; @@ -31,7 +34,7 @@ export const Route = createFileRoute("/signup")({ }) }); -type SignUpMethod = "email" | "github" | "google" | "apple" | null; +type SignUpMethod = "email" | "github" | "google" | "apple" | "guest" | null; function SignupPage() { const navigate = useNavigate(); @@ -40,14 +43,17 @@ function SignupPage() { const [signUpMethod, setSignUpMethod] = useState(null); const [error, setError] = useState(null); const [isLoading, setIsLoading] = useState(false); + const [showGuestWarning, setShowGuestWarning] = useState(false); + const [showGuestCredentials, setShowGuestCredentials] = useState(false); + const [guestUuid, setGuestUuid] = useState(null); // Use platform detection functions const isIOSPlatform = isIOS(); const isTauriEnv = isTauri(); - // Redirect if already logged in + // Redirect if already logged in (but not if we're showing guest credentials) useEffect(() => { - if (os.auth.user) { + if (os.auth.user && !showGuestCredentials) { if (selected_plan) { navigate({ to: "/pricing", @@ -57,7 +63,7 @@ function SignupPage() { navigate({ to: next || "/" }); } } - }, [os.auth.user, navigate, next, selected_plan]); + }, [os.auth.user, navigate, next, selected_plan, showGuestCredentials]); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); @@ -166,6 +172,47 @@ function SignupPage() { } }; + const handleGuestWarningAccept = () => { + setShowGuestWarning(false); + setSignUpMethod("guest"); + }; + + const handleGuestSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setIsLoading(true); + setError(null); + const formData = new FormData(e.currentTarget); + const password = formData.get("password") as string; + + try { + const response: LoginResponse = await os.signUpGuest(password, ""); + setGuestUuid(response.id); + // Clear any existing billing token to prevent session mixing + try { + getBillingService().clearToken(); + } catch (billingError) { + console.warn("Failed to clear billing token:", billingError); + } + // Show credentials dialog + setShowGuestCredentials(true); + } catch (error) { + console.error(error); + if (error instanceof Error) { + setError(`${error.message}`); + } else { + setError("Something went wrong. Please try again."); + } + } finally { + setIsLoading(false); + } + }; + + const handleGuestCredentialsContinue = () => { + setShowGuestCredentials(false); + // Navigate to pricing page (guests need to pay immediately) + navigate({ to: "/pricing" }); + }; + const handleAppleSignup = async () => { try { if (isTauriEnv && isIOSPlatform) { @@ -306,16 +353,78 @@ function SignupPage() { inviteCode="" /> )} +
Already have an account?{" "} Log In
+ ); } + if (signUpMethod === "guest") { + return ( + <> + + + {error && } +
+ + +

+ You'll need this password along with your Account ID to sign in. +

+
+ + +
+ + {guestUuid && ( + + )} + + ); + } + return (
From 600e1235ce3f1521b2796fe73b56dadf6f162646 Mon Sep 17 00:00:00 2001 From: Tony Giorgio Date: Tue, 28 Oct 2025 16:55:15 -0500 Subject: [PATCH 2/4] Fix: Skip email verification modal for guest users Guest users don't have email addresses, so they should not be prompted to verify their email. This was causing issues in production (non-dev mode). Changes: - Skip verification modal for guest users in initial state - Skip verification modal for guest users in useEffect - Matches same pattern used for dev mode bypass Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> --- frontend/src/components/VerificationModal.tsx | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/frontend/src/components/VerificationModal.tsx b/frontend/src/components/VerificationModal.tsx index b0ad48dc7..c3d8f974d 100644 --- a/frontend/src/components/VerificationModal.tsx +++ b/frontend/src/components/VerificationModal.tsx @@ -17,8 +17,9 @@ export function VerificationModal() { const os = useOpenSecret(); const [isOpen, setIsOpen] = useState(() => { if (!os.auth.user) return false; - // Skip email verification in local development - return !import.meta.env.DEV && !os.auth.user.user.email_verified; + // Skip email verification for guest users and in local development + const isGuestUser = os.auth.user.user.login_method?.toLowerCase() === "guest"; + return !import.meta.env.DEV && !isGuestUser && !os.auth.user.user.email_verified; }); const [isResending, setIsResending] = useState(false); const [justResent, setJustResent] = useState(false); @@ -35,15 +36,18 @@ export function VerificationModal() { setError(null); setJustResent(false); } else { - // Skip email verification in local development - const shouldRequireVerification = !import.meta.env.DEV && !os.auth.user.user.email_verified; + // Skip email verification for guest users and in local development + const isGuestUser = os.auth.user.user.login_method?.toLowerCase() === "guest"; + const shouldRequireVerification = + !import.meta.env.DEV && !isGuestUser && !os.auth.user.user.email_verified; setIsOpen(shouldRequireVerification); } }, [os.auth.user]); const handleOpenChange = (open: boolean) => { - // Allow closing if the email is verified or in local development - if (!open && (os.auth.user?.user.email_verified || import.meta.env.DEV)) { + // Allow closing if the email is verified, in local development, or guest user + const isGuestUser = os.auth.user?.user.login_method?.toLowerCase() === "guest"; + if (!open && (os.auth.user?.user.email_verified || import.meta.env.DEV || isGuestUser)) { setIsOpen(false); // Reset form state when modal closes setVerificationCode(""); From 0c882aea8049cce8eb9b839a5ae2acbc547bff3c Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Fri, 31 Oct 2025 00:12:55 +0000 Subject: [PATCH 3/4] Add clarifying text about Account ID display in signup flow Co-authored-by: Anthony --- frontend/src/components/GuestSignupWarningDialog.tsx | 12 +++++++++--- frontend/src/routes/signup.tsx | 3 ++- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/GuestSignupWarningDialog.tsx b/frontend/src/components/GuestSignupWarningDialog.tsx index d1bb20f8d..9434e19e0 100644 --- a/frontend/src/components/GuestSignupWarningDialog.tsx +++ b/frontend/src/components/GuestSignupWarningDialog.tsx @@ -59,7 +59,9 @@ export function GuestSignupWarningDialog({ setBitcoinPaymentAgreed(checked === true)} + onCheckedChange={(checked: boolean | "indeterminate") => + setBitcoinPaymentAgreed(checked === true) + } />