feat: add email verification check for cloud onboarding (#5636)

## Summary
- Added email verification flow for new users during onboarding
- Implemented invite code claiming with proper validation 
- Updated API endpoints from `/invite/` to `/invite_code/` for
consistency

## Changes

### Email Verification
- Added `CloudVerifyEmailView` component with email verification UI
- Added email verification check after login in `CloudLoginView`
- Added `isEmailVerified` property to Firebase auth store
- Users must verify email before claiming invite codes

### Invite Code Flow
- Enhanced `CloudClaimInviteView` with full claim invite functionality
- Updated `InviteCheckView` to route users based on email verification
status
- Modified API to return both `claimed` and `expired` status for invite
codes
- Added proper error handling and Sentry logging for invite operations

### API Updates
- Changed endpoint paths from `/invite/` to `/invite_code/` 
- Updated `getInviteCodeStatus()` to return `{ claimed: boolean;
expired: boolean }`
- Updated `claimInvite()` to return `{ success: boolean; message: string
}`

### UI/UX Improvements
- Added Korean translations for all new strings
- Improved button styling and layout in survey and waitlist views
- Added proper loading states and error handling

## Test Plan
- [ ] Test new user signup flow with email verification
- [ ] Test invite code validation (expired/claimed/valid codes)
- [ ] Test email verification redirect flow
- [ ] Test invite claiming after email verification
- [ ] Verify Korean translations display correctly

🤖 Generated with [Claude Code](https://claude.ai/code)

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Jin Yi
2025-09-21 12:29:56 +09:00
committed by GitHub
parent d3a5d9e995
commit 8ca541e850
15 changed files with 423 additions and 130 deletions

View File

@@ -88,10 +88,10 @@ export async function getUserCloudStatus(): Promise<UserCloudStatus> {
export async function getInviteCodeStatus(
inviteCode: string
): Promise<{ expired: boolean }> {
): Promise<{ claimed: boolean; expired: boolean }> {
try {
const response = await api.fetchApi(
`/invite/${encodeURIComponent(inviteCode)}/status`,
`/invite_code/${encodeURIComponent(inviteCode)}/status`,
{
method: 'GET',
headers: {
@@ -105,22 +105,22 @@ export async function getInviteCodeStatus(
)
captureApiError(
error,
'/invite/{code}/status',
'/invite_code/{code}/status',
'http_error',
response.status,
undefined,
{
api: {
method: 'GET',
endpoint: `/invite/${inviteCode}/status`,
endpoint: `/invite_code/${inviteCode}/status`,
status_code: response.status,
status_text: response.statusText
},
extra: {
invite_code_length: inviteCode.length
},
route_template: '/invite/{code}/status',
route_actual: `/invite/${inviteCode}/status`
route_template: '/invite_code/{code}/status',
route_actual: `/invite_code/${inviteCode}/status`
}
)
throw error
@@ -132,13 +132,13 @@ export async function getInviteCodeStatus(
if (!isHttpError(error, 'Failed to get invite code status:')) {
captureApiError(
error as Error,
'/invite/{code}/status',
'/invite_code/{code}/status',
'network_error',
undefined,
undefined,
{
route_template: '/invite/{code}/status',
route_actual: `/invite/${inviteCode}/status`
route_template: '/invite_code/{code}/status',
route_actual: `/invite_code/${inviteCode}/status`
}
)
}
@@ -293,7 +293,9 @@ export async function submitSurvey(
}
}
export async function claimInvite(code: string): Promise<void> {
export async function claimInvite(
code: string
): Promise<{ success: boolean; message: string }> {
try {
Sentry.addBreadcrumb({
category: 'auth',
@@ -305,7 +307,7 @@ export async function claimInvite(code: string): Promise<void> {
})
const res = await api.fetchApi(
`/invite/${encodeURIComponent(code)}/claim`,
`/invite_code/${encodeURIComponent(code)}/claim`,
{
method: 'POST'
}
@@ -317,7 +319,7 @@ export async function claimInvite(code: string): Promise<void> {
)
captureApiError(
error,
'/invite/{code}/claim',
'/invite_code/{code}/claim',
'http_error',
res.status,
'claim_invite',
@@ -327,8 +329,8 @@ export async function claimInvite(code: string): Promise<void> {
status_code: res.status,
status_text: res.statusText
},
route_template: '/invite/{code}/claim',
route_actual: `/invite/${encodeURIComponent(code)}/claim`
route_template: '/invite_code/{code}/claim',
route_actual: `/invite_code/${encodeURIComponent(code)}/claim`
}
)
throw error
@@ -340,18 +342,20 @@ export async function claimInvite(code: string): Promise<void> {
message: 'Invite claimed successfully',
level: 'info'
})
return res.json()
} catch (error) {
// Only capture network errors (not HTTP errors we already captured)
if (!isHttpError(error, 'Failed to claim invite:')) {
captureApiError(
error as Error,
'/invite/{code}/claim',
'/invite_code/{code}/claim',
'network_error',
undefined,
'claim_invite',
{
route_template: '/invite/{code}/claim',
route_actual: `/invite/${encodeURIComponent(code)}/claim`
route_template: '/invite_code/{code}/claim',
route_actual: `/invite_code/${encodeURIComponent(code)}/claim`
}
)
}