@stjoseph/prumo 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.prumo-templates/api/auth.md +51 -60
- package/.prumo-templates/api/config.md +2 -2
- package/.prumo-templates/api/routes.md +6 -4
- package/.prumo-templates/api/structure.md +4 -1
- package/.prumo-templates/client/data.md +16 -18
- package/.prumo-templates/client/forms.md +2 -2
- package/.prumo-templates/core/testing.md +4 -4
- package/.prumo-templates/database/testing.md +6 -8
- package/.prumo-templates/email/codes.md +62 -0
- package/.prumo-templates/email/mailer.md +47 -0
- package/.prumo-templates/mcp/server.md +55 -14
- package/.prumo-templates/mobile/routing.md +1 -1
- package/.prumo-templates/mobile/storage.md +7 -6
- package/.prumo-templates/monorepo/layout.md +4 -4
- package/.prumo-templates/multi-tenancy/client.md +6 -6
- package/.prumo-templates/multi-tenancy/isolation.md +17 -10
- package/.prumo-templates/social/sign-in.md +77 -0
- package/README.md +8 -6
- package/dist/asker.js +13 -0
- package/dist/clean.js +2 -2
- package/dist/cli.js +16 -2
- package/dist/commands.js +7 -0
- package/dist/compose.js +3 -11
- package/dist/context.js +4 -0
- package/dist/features.js +230 -0
- package/dist/generate.js +20 -7
- package/dist/questions.js +64 -1
- package/package.json +1 -1
- package/templates/api/.env.example +21 -5
- package/templates/api/README.md +4 -5
- package/templates/api/docker-compose.yml +0 -1
- package/templates/api/migrations/Migration20261005111545_create_user_profile_session.ts +31 -0
- package/templates/api/migrations/Migration20261005111547_create_authorization.ts +18 -0
- package/templates/api/migrations/Migration20261005111549_create_email_code.ts +18 -0
- package/templates/api/migrations/Migration20261005111550_create_identity_social_sign_in.ts +25 -0
- package/templates/api/package.json +7 -8
- package/templates/api/scripts/database.mjs +3 -11
- package/templates/api/scripts/ports.mjs +1 -1
- package/templates/api/src/domain/auth/dto/email-request.dto.ts +5 -0
- package/templates/api/src/domain/auth/dto/refresh.dto.ts +9 -0
- package/templates/api/src/domain/auth/dto/reset-password.dto.ts +9 -0
- package/templates/api/src/domain/auth/dto/session-response.dto.ts +7 -0
- package/templates/api/src/domain/auth/dto/sign-in.dto.ts +8 -0
- package/templates/api/src/domain/auth/dto/sign-up.dto.ts +9 -0
- package/templates/api/src/domain/auth/dto/tokens-response.dto.ts +10 -0
- package/templates/api/src/domain/auth/dto/verify-email.dto.ts +8 -0
- package/templates/api/src/domain/auth/entities/email-code.entity.ts +46 -0
- package/templates/api/src/domain/auth/entities/identity.entity.ts +18 -0
- package/templates/api/src/domain/auth/entities/session.entity.ts +78 -0
- package/templates/api/src/domain/auth/entities/social-sign-in.entity.ts +82 -0
- package/templates/api/src/domain/auth/entities/user.entity.ts +34 -0
- package/templates/api/src/domain/auth/errors/email-not-verified.error.ts +10 -0
- package/templates/api/src/domain/auth/errors/email-taken.error.ts +9 -0
- package/templates/api/src/domain/auth/errors/invalid-code.error.ts +11 -0
- package/templates/api/src/domain/auth/errors/invalid-credentials.error.ts +10 -0
- package/templates/api/src/domain/auth/errors/invalid-session.error.ts +9 -0
- package/templates/api/src/domain/auth/errors/provider-email-unverified.error.ts +11 -0
- package/templates/api/src/domain/auth/errors/provider-not-configured.error.ts +10 -0
- package/templates/api/src/domain/auth/errors/social-sign-in-failed.error.ts +11 -0
- package/templates/api/src/domain/auth/errors/superseded-token.error.ts +11 -0
- package/templates/api/src/domain/auth/ports/access-tokens.port.ts +15 -0
- package/templates/api/src/domain/auth/ports/identity-providers.port.ts +29 -0
- package/templates/api/src/domain/auth/ports/mailer.port.ts +8 -0
- package/templates/api/src/domain/auth/ports/one-time-codes.port.ts +7 -0
- package/templates/api/src/domain/auth/ports/opaque-tokens.port.ts +7 -0
- package/templates/api/src/domain/auth/ports/password-hasher.port.ts +8 -0
- package/templates/api/src/domain/auth/repositories/email-code.repository.ts +9 -0
- package/templates/api/src/domain/auth/repositories/identity.repository.ts +9 -0
- package/templates/api/src/domain/auth/repositories/session.repository.ts +12 -0
- package/templates/api/src/domain/auth/repositories/social-sign-in.repository.ts +9 -0
- package/templates/api/src/domain/auth/repositories/user.repository.ts +9 -0
- package/templates/api/src/domain/auth/use-cases/complete-social-sign-in.use-case.ts +103 -0
- package/templates/api/src/domain/auth/use-cases/exchange-social-code.use-case.ts +56 -0
- package/templates/api/src/domain/auth/use-cases/find-user.use-case.ts +20 -0
- package/templates/api/src/domain/auth/use-cases/issue-email-code.use-case.ts +55 -0
- package/templates/api/src/domain/auth/use-cases/link-identity.use-case.ts +77 -0
- package/templates/api/src/domain/auth/use-cases/refresh-session.use-case.ts +94 -0
- package/templates/api/src/domain/auth/use-cases/request-email-verification.use-case.ts +21 -0
- package/templates/api/src/domain/auth/use-cases/request-password-reset.use-case.ts +21 -0
- package/templates/api/src/domain/auth/use-cases/reset-password.use-case.ts +73 -0
- package/templates/api/src/domain/auth/use-cases/sign-in.use-case.ts +38 -0
- package/templates/api/src/domain/auth/use-cases/sign-out.use-case.ts +22 -0
- package/templates/api/src/domain/auth/use-cases/sign-up.use-case.ts +57 -0
- package/templates/api/src/domain/auth/use-cases/start-session.use-case.ts +55 -0
- package/templates/api/src/domain/auth/use-cases/start-social-sign-in.use-case.ts +54 -0
- package/templates/api/src/domain/auth/use-cases/verify-email.use-case.ts +62 -0
- package/templates/api/src/domain/oauth/dto/authorization-decision.dto.ts +7 -0
- package/templates/api/src/domain/oauth/dto/authorization-view.dto.ts +14 -0
- package/templates/api/src/domain/oauth/entities/authorization.entity.ts +87 -0
- package/templates/api/src/domain/oauth/errors/authorization-not-found.error.ts +9 -0
- package/templates/api/src/domain/oauth/errors/authorization-request.error.ts +18 -0
- package/templates/api/src/domain/oauth/errors/invalid-client.error.ts +6 -0
- package/templates/api/src/domain/oauth/errors/invalid-grant.error.ts +6 -0
- package/templates/api/src/domain/oauth/ports/authorization-codes.port.ts +8 -0
- package/templates/api/src/domain/oauth/ports/client-metadata.port.ts +9 -0
- package/templates/api/src/domain/oauth/repositories/authorization.repository.ts +10 -0
- package/templates/api/src/domain/oauth/use-cases/decide-authorization.use-case.ts +53 -0
- package/templates/api/src/domain/oauth/use-cases/exchange-code.use-case.ts +97 -0
- package/templates/api/src/domain/oauth/use-cases/find-authorization.use-case.ts +24 -0
- package/templates/api/src/domain/oauth/use-cases/start-authorization.use-case.ts +89 -0
- package/templates/api/src/domain/shared/errors/domain-error.ts +2 -0
- package/templates/api/src/infra/auth/argon2-password-hasher.adapter.ts +21 -0
- package/templates/api/src/infra/auth/crypto-opaque-tokens.adapter.ts +14 -0
- package/templates/api/src/infra/auth/jose-access-tokens.adapter.ts +64 -0
- package/templates/api/src/infra/config/env.ts +17 -3
- package/templates/api/src/infra/database/mikroorm/entities/authorization.schema.ts +38 -0
- package/templates/api/src/infra/database/mikroorm/entities/email-code.schema.ts +30 -0
- package/templates/api/src/infra/database/mikroorm/entities/identity.schema.ts +29 -0
- package/templates/api/src/infra/database/mikroorm/entities/profile.schema.ts +1 -1
- package/templates/api/src/infra/database/mikroorm/entities/session.schema.ts +34 -0
- package/templates/api/src/infra/database/mikroorm/entities/social-sign-in.schema.ts +35 -0
- package/templates/api/src/infra/database/mikroorm/entities/user.schema.ts +19 -0
- package/templates/api/src/infra/database/mikroorm/mikro-orm.factory.ts +15 -4
- package/templates/api/src/infra/database/mikroorm/repositories/mikroorm-authorization.repository.ts +21 -0
- package/templates/api/src/infra/database/mikroorm/repositories/mikroorm-email-code.repository.ts +17 -0
- package/templates/api/src/infra/database/mikroorm/repositories/mikroorm-identity.repository.ts +18 -0
- package/templates/api/src/infra/database/mikroorm/repositories/mikroorm-session.repository.ts +25 -0
- package/templates/api/src/infra/database/mikroorm/repositories/mikroorm-social-sign-in.repository.ts +17 -0
- package/templates/api/src/infra/database/mikroorm/repositories/mikroorm-user.repository.ts +31 -0
- package/templates/api/src/infra/di/auth.di.ts +21 -0
- package/templates/api/src/infra/di/email.di.ts +23 -0
- package/templates/api/src/infra/di/index.ts +8 -10
- package/templates/api/src/infra/di/oauth.di.ts +14 -0
- package/templates/api/src/infra/di/social.di.ts +17 -0
- package/templates/api/src/infra/email/hmac-one-time-codes.adapter.ts +16 -0
- package/templates/api/src/infra/email/log-mailer.adapter.ts +11 -0
- package/templates/api/src/infra/http/app.ts +15 -1
- package/templates/api/src/infra/http/controllers/auth-email.controller.ts +73 -0
- package/templates/api/src/infra/http/controllers/auth.controller.ts +108 -29
- package/templates/api/src/infra/http/controllers/mcp.controller.ts +46 -27
- package/templates/api/src/infra/http/controllers/oauth.controller.ts +267 -0
- package/templates/api/src/infra/http/controllers/social.controller.ts +192 -0
- package/templates/api/src/infra/http/errors/error-handler.ts +5 -0
- package/templates/api/src/infra/http/fastify.d.ts +2 -3
- package/templates/api/src/infra/http/fetch-bridge.ts +17 -5
- package/templates/api/src/infra/http/hooks/auth.hook.ts +25 -8
- package/templates/api/src/infra/http/hooks/origin.hook.ts +22 -0
- package/templates/api/src/infra/http/server.ts +1 -4
- package/templates/api/src/infra/http/session-delivery.ts +69 -0
- package/templates/api/src/infra/mcp/mcp.scopes.ts +27 -0
- package/templates/api/src/infra/mcp/mcp.server.ts +10 -0
- package/templates/api/src/infra/mcp/tools/users.tool.ts +6 -0
- package/templates/api/src/infra/oauth/crypto-authorization-codes.adapter.ts +21 -0
- package/templates/api/src/infra/oauth/https-client-metadata.adapter.ts +231 -0
- package/templates/api/src/infra/social/openid-identity-providers.adapter.ts +199 -0
- package/templates/api/test/domain/auth/entities/session.entity.spec.ts +50 -0
- package/templates/api/test/domain/auth/use-cases/email.use-case.spec.ts +87 -0
- package/templates/api/test/domain/auth/use-cases/refresh-session.use-case.spec.ts +66 -0
- package/templates/api/test/domain/auth/use-cases/sign-in.use-case.spec.ts +29 -0
- package/templates/api/test/domain/auth/use-cases/sign-up.use-case.spec.ts +41 -0
- package/templates/api/test/domain/auth/use-cases/social.use-case.spec.ts +148 -0
- package/templates/api/test/domain/oauth/entities/authorization.entity.spec.ts +53 -0
- package/templates/api/test/infra/auth/argon2-password-hasher.adapter.spec.ts +17 -0
- package/templates/api/test/infra/auth/jose-access-tokens.adapter.spec.ts +57 -0
- package/templates/api/test/infra/database/mikroorm/mikro-orm.factory.spec.ts +1 -1
- package/templates/api/test/infra/database/mikroorm/repositories/mikroorm-user.repository.spec.ts +28 -0
- package/templates/api/test/infra/email/hmac-one-time-codes.adapter.spec.ts +15 -0
- package/templates/api/test/infra/http/controllers/auth-email.controller.spec.ts +92 -0
- package/templates/api/test/infra/http/controllers/auth.controller.spec.ts +168 -0
- package/templates/api/test/infra/http/controllers/mcp.controller.spec.ts +309 -67
- package/templates/api/test/infra/http/controllers/social.controller.spec.ts +108 -0
- package/templates/api/test/infra/http/controllers/users.controller.spec.ts +13 -35
- package/templates/api/test/infra/oauth/crypto-authorization-codes.adapter.spec.ts +18 -0
- package/templates/api/test/infra/oauth/https-client-metadata.adapter.spec.ts +58 -0
- package/templates/api/test/infra/social/openid-identity-providers.adapter.spec.ts +154 -0
- package/templates/api/test/support/factories/user.factory.ts +9 -11
- package/templates/api/test/support/fake-oidc-server.ts +96 -0
- package/templates/api/test/support/fakes/auth-fakes.ts +94 -0
- package/templates/api/test/support/fakes/fake-access-tokens.ts +19 -0
- package/templates/api/test/support/fakes/fake-mailer.ts +21 -0
- package/templates/api/test/support/fakes/fake-one-time-codes.ts +14 -0
- package/templates/api/test/support/fakes/fake-opaque-tokens.ts +14 -0
- package/templates/api/test/support/fakes/fake-password-hasher.ts +15 -0
- package/templates/api/test/support/fakes/immediate-transaction-manager.ts +8 -0
- package/templates/api/test/support/fakes/in-memory-email-code.repository.ts +15 -0
- package/templates/api/test/support/fakes/in-memory-session.repository.ts +27 -0
- package/templates/api/test/support/fakes/in-memory-user.repository.ts +20 -0
- package/templates/api/test/support/fakes/social-fakes.ts +112 -0
- package/templates/api/test/support/setup.ts +6 -28
- package/templates/api/test/support/test-app.ts +65 -0
- package/templates/mobile/README.md +1 -1
- package/templates/mobile/jest.config.js +0 -17
- package/templates/mobile/package.json +0 -4
- package/templates/mobile/scripts/ports.mjs +1 -1
- package/templates/mobile/src/api-contract/api-error.ts +4 -11
- package/templates/mobile/src/api-contract/auth.ts +221 -0
- package/templates/mobile/src/api-contract/index.ts +14 -0
- package/templates/mobile/src/app/(app)/_layout.tsx +1 -1
- package/templates/mobile/src/app/(auth)/forgot-password.tsx +20 -0
- package/templates/mobile/src/app/(auth)/reset-password.tsx +18 -0
- package/templates/mobile/src/app/(auth)/sign-in.tsx +9 -0
- package/templates/mobile/src/app/(auth)/sign-up.tsx +4 -0
- package/templates/mobile/src/app/(auth)/verify-email.tsx +18 -0
- package/templates/mobile/src/app/_layout.tsx +5 -5
- package/templates/mobile/src/features/auth/forgot-password-form.tsx +67 -0
- package/templates/mobile/src/features/auth/reset-password-form.tsx +93 -0
- package/templates/mobile/src/features/auth/sign-in-form.tsx +21 -7
- package/templates/mobile/src/features/auth/sign-out-button.tsx +4 -4
- package/templates/mobile/src/features/auth/sign-up-form.tsx +24 -8
- package/templates/mobile/src/features/auth/social-buttons.tsx +79 -0
- package/templates/mobile/src/features/auth/token-store.ts +21 -0
- package/templates/mobile/src/features/auth/verify-email-form.tsx +81 -0
- package/templates/mobile/src/features/clients/clients-context.tsx +1 -2
- package/templates/mobile/test/features/auth/authenticated-layout.spec.tsx +16 -14
- package/templates/mobile/test/features/auth/email-flows.spec.tsx +86 -0
- package/templates/mobile/test/features/auth/sign-in-form.spec.tsx +12 -11
- package/templates/mobile/test/features/auth/sign-out-button.spec.tsx +8 -9
- package/templates/mobile/test/features/auth/social-buttons.spec.tsx +77 -0
- package/templates/mobile/test/support/memory-token-store.ts +15 -0
- package/templates/site/scripts/ports.mjs +1 -1
- package/templates/web/package.json +0 -3
- package/templates/web/scripts/ports.mjs +1 -1
- package/templates/web/src/api-contract/api-error.ts +4 -11
- package/templates/web/src/api-contract/auth.ts +221 -0
- package/templates/web/src/api-contract/index.ts +14 -0
- package/templates/web/src/features/auth/forgot-password-form.tsx +52 -0
- package/templates/web/src/features/auth/reset-password-form.tsx +82 -0
- package/templates/web/src/features/auth/sign-in-form.tsx +24 -14
- package/templates/web/src/features/auth/sign-out-button.tsx +4 -4
- package/templates/web/src/features/auth/sign-up-form.tsx +16 -8
- package/templates/web/src/features/auth/social-buttons.tsx +45 -0
- package/templates/web/src/features/auth/verify-email-form.tsx +84 -0
- package/templates/web/src/features/oauth/consent-form.tsx +72 -35
- package/templates/web/src/main.tsx +6 -4
- package/templates/web/src/routeTree.gen.ts +73 -2
- package/templates/web/src/router.tsx +1 -2
- package/templates/web/src/routes/_authenticated/consent.tsx +5 -4
- package/templates/web/src/routes/_authenticated.tsx +1 -1
- package/templates/web/src/routes/forgot-password.tsx +17 -0
- package/templates/web/src/routes/reset-password.tsx +20 -0
- package/templates/web/src/routes/sign-in.tsx +10 -3
- package/templates/web/src/routes/sign-up.tsx +4 -0
- package/templates/web/src/routes/verify-email.tsx +20 -0
- package/templates/web/test/api-contract/auth.spec.ts +155 -0
- package/templates/web/test/app.spec.tsx +47 -22
- package/templates/web/test/features/auth/email-flows.spec.tsx +121 -0
- package/templates/web/test/features/auth/sign-in-form.spec.tsx +6 -2
- package/templates/web/test/features/auth/social-buttons.spec.tsx +35 -0
- package/templates/web/test/features/oauth/consent-form.spec.tsx +90 -26
- package/templates/web/test/support/render-app.tsx +4 -4
- package/templates/workspace/scripts/ports.mjs +1 -1
- package/dist/mcp.js +0 -100
- package/templates/api/docker/init/01-auth-schema.sql +0 -1
- package/templates/api/migrations/Migration20260914123239_create_profile.ts +0 -13
- package/templates/api/src/infra/auth/auth.cli.ts +0 -5
- package/templates/api/src/infra/auth/auth.factory.ts +0 -77
- package/templates/api/src/infra/database/mikroorm/entities/auth-user.schema.ts +0 -16
- package/templates/api/test/infra/auth/auth.factory.spec.ts +0 -22
- package/templates/mobile/src/features/auth/auth-client.ts +0 -27
- package/templates/mobile/src/features/auth/session.ts +0 -17
- package/templates/web/src/features/auth/auth-client.ts +0 -17
- package/templates/web/src/features/auth/session.ts +0 -17
|
@@ -2,6 +2,7 @@ import { Link, useRouter } from 'expo-router'
|
|
|
2
2
|
import { Text, View } from 'react-native'
|
|
3
3
|
import { SafeAreaView } from 'react-native-safe-area-context'
|
|
4
4
|
import { SignUpForm } from '@/features/auth/sign-up-form'
|
|
5
|
+
import { SocialButtons } from '@/features/auth/social-buttons' // prumo:social
|
|
5
6
|
import { useClients } from '@/features/clients/clients-context'
|
|
6
7
|
|
|
7
8
|
export default function SignUpScreen() {
|
|
@@ -11,6 +12,9 @@ export default function SignUpScreen() {
|
|
|
11
12
|
return (
|
|
12
13
|
<SafeAreaView className="flex-1 justify-center gap-6 p-6">
|
|
13
14
|
<Text className="text-2xl font-semibold text-neutral-900">Create an account</Text>
|
|
15
|
+
{/* prumo:social */}
|
|
16
|
+
<SocialButtons auth={auth} onSignedIn={() => router.replace('/')} />
|
|
17
|
+
{/* prumo:end-social */}
|
|
14
18
|
<SignUpForm auth={auth} onSignedUp={() => router.replace('/')} />
|
|
15
19
|
<View className="flex-row gap-1">
|
|
16
20
|
<Text className="text-sm text-neutral-500">Already registered?</Text>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { useLocalSearchParams, useRouter } from 'expo-router'
|
|
2
|
+
import { Text } from 'react-native'
|
|
3
|
+
import { SafeAreaView } from 'react-native-safe-area-context'
|
|
4
|
+
import { VerifyEmailForm } from '@/features/auth/verify-email-form'
|
|
5
|
+
import { useClients } from '@/features/clients/clients-context'
|
|
6
|
+
|
|
7
|
+
export default function VerifyEmailScreen() {
|
|
8
|
+
const { auth } = useClients()
|
|
9
|
+
const router = useRouter()
|
|
10
|
+
const { email } = useLocalSearchParams<{ email: string }>()
|
|
11
|
+
|
|
12
|
+
return (
|
|
13
|
+
<SafeAreaView className="flex-1 justify-center gap-6 p-6">
|
|
14
|
+
<Text className="text-2xl font-semibold text-neutral-900">Confirm your email</Text>
|
|
15
|
+
<VerifyEmailForm auth={auth} email={email} onVerified={() => router.replace('/')} />
|
|
16
|
+
</SafeAreaView>
|
|
17
|
+
)
|
|
18
|
+
}
|
|
@@ -4,18 +4,18 @@ import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client
|
|
|
4
4
|
import { Stack } from 'expo-router'
|
|
5
5
|
import * as SplashScreen from 'expo-splash-screen'
|
|
6
6
|
import { useEffect, useState } from 'react'
|
|
7
|
-
import { createClient } from '@/api-contract'
|
|
7
|
+
import { createAuthClient, createClient, sessionQuery } from '@/api-contract'
|
|
8
8
|
import { config } from '@/config'
|
|
9
|
-
import {
|
|
10
|
-
import { sessionQuery } from '@/features/auth/session'
|
|
9
|
+
import { secureTokenStore } from '@/features/auth/token-store'
|
|
11
10
|
import { ClientsProvider, useClients } from '@/features/clients/clients-context'
|
|
12
11
|
import { QUERY_CACHE_MAX_AGE, queryPersister } from '@/features/storage/query-persister'
|
|
13
12
|
|
|
14
13
|
SplashScreen.preventAutoHideAsync()
|
|
15
14
|
|
|
16
15
|
function createClients() {
|
|
17
|
-
const auth =
|
|
18
|
-
|
|
16
|
+
const auth = createAuthClient({ baseUrl: config.apiUrl, fetch, tokens: secureTokenStore })
|
|
17
|
+
// Through the auth client, so every request carries the bearer and an expired one is refreshed and repeated.
|
|
18
|
+
const api = createClient({ baseUrl: config.apiUrl, fetch: auth.fetch })
|
|
19
19
|
|
|
20
20
|
return { auth, api }
|
|
21
21
|
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { zodResolver } from '@hookform/resolvers/zod'
|
|
2
|
+
import { Controller, useForm } from 'react-hook-form'
|
|
3
|
+
import { View } from 'react-native'
|
|
4
|
+
import { z } from 'zod'
|
|
5
|
+
import type { AuthClient } from '@/api-contract'
|
|
6
|
+
import { Button } from '@/components/ui/button'
|
|
7
|
+
import { Field } from '@/components/ui/field'
|
|
8
|
+
import { FormError } from '@/features/forms/form-error'
|
|
9
|
+
import { applyServerError } from '@/features/forms/server-errors'
|
|
10
|
+
|
|
11
|
+
const schema = z.object({ email: z.email() })
|
|
12
|
+
|
|
13
|
+
type ForgotPasswordValues = z.infer<typeof schema>
|
|
14
|
+
|
|
15
|
+
export function ForgotPasswordForm({
|
|
16
|
+
auth,
|
|
17
|
+
onSent,
|
|
18
|
+
}: {
|
|
19
|
+
auth: Pick<AuthClient, 'requestPasswordReset'>
|
|
20
|
+
onSent: (email: string) => void
|
|
21
|
+
}) {
|
|
22
|
+
const form = useForm<ForgotPasswordValues>({
|
|
23
|
+
resolver: zodResolver(schema),
|
|
24
|
+
mode: 'onTouched',
|
|
25
|
+
defaultValues: { email: '' },
|
|
26
|
+
})
|
|
27
|
+
const { errors, isSubmitting } = form.formState
|
|
28
|
+
|
|
29
|
+
async function onSubmit(values: ForgotPasswordValues) {
|
|
30
|
+
try {
|
|
31
|
+
await auth.requestPasswordReset(values.email)
|
|
32
|
+
} catch (error) {
|
|
33
|
+
applyServerError(error, ['email'], form.setError)
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// The API answers alike whether the account exists, so the next screen cannot tell either.
|
|
38
|
+
onSent(values.email)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<View className="gap-6">
|
|
43
|
+
<FormError errors={errors} />
|
|
44
|
+
<Controller
|
|
45
|
+
control={form.control}
|
|
46
|
+
name="email"
|
|
47
|
+
render={({ field }) => (
|
|
48
|
+
<Field
|
|
49
|
+
label="Email"
|
|
50
|
+
autoCapitalize="none"
|
|
51
|
+
autoComplete="email"
|
|
52
|
+
keyboardType="email-address"
|
|
53
|
+
value={field.value}
|
|
54
|
+
onChangeText={field.onChange}
|
|
55
|
+
onBlur={field.onBlur}
|
|
56
|
+
error={errors.email?.message}
|
|
57
|
+
/>
|
|
58
|
+
)}
|
|
59
|
+
/>
|
|
60
|
+
<Button
|
|
61
|
+
label="Send a reset code"
|
|
62
|
+
disabled={isSubmitting}
|
|
63
|
+
onPress={form.handleSubmit(onSubmit)}
|
|
64
|
+
/>
|
|
65
|
+
</View>
|
|
66
|
+
)
|
|
67
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { zodResolver } from '@hookform/resolvers/zod'
|
|
2
|
+
import { useQueryClient } from '@tanstack/react-query'
|
|
3
|
+
import { Controller, useForm } from 'react-hook-form'
|
|
4
|
+
import { Text, View } from 'react-native'
|
|
5
|
+
import { z } from 'zod'
|
|
6
|
+
import { type AuthClient, sessionQuery } from '@/api-contract'
|
|
7
|
+
import { Button } from '@/components/ui/button'
|
|
8
|
+
import { Field } from '@/components/ui/field'
|
|
9
|
+
import { FormError } from '@/features/forms/form-error'
|
|
10
|
+
import { applyServerError } from '@/features/forms/server-errors'
|
|
11
|
+
|
|
12
|
+
const schema = z.object({
|
|
13
|
+
code: z.string().regex(/^\d{6}$/, 'Enter the 6 digits from the email'),
|
|
14
|
+
password: z.string().min(8).max(128),
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
type ResetPasswordValues = z.infer<typeof schema>
|
|
18
|
+
|
|
19
|
+
export function ResetPasswordForm({
|
|
20
|
+
auth,
|
|
21
|
+
email,
|
|
22
|
+
onReset,
|
|
23
|
+
}: {
|
|
24
|
+
auth: Pick<AuthClient, 'resetPassword' | 'getSession'>
|
|
25
|
+
email: string
|
|
26
|
+
onReset: () => void
|
|
27
|
+
}) {
|
|
28
|
+
const queryClient = useQueryClient()
|
|
29
|
+
const form = useForm<ResetPasswordValues>({
|
|
30
|
+
resolver: zodResolver(schema),
|
|
31
|
+
mode: 'onTouched',
|
|
32
|
+
defaultValues: { code: '', password: '' },
|
|
33
|
+
})
|
|
34
|
+
const { errors, isSubmitting } = form.formState
|
|
35
|
+
|
|
36
|
+
async function onSubmit(values: ResetPasswordValues) {
|
|
37
|
+
try {
|
|
38
|
+
await auth.resetPassword({ email, ...values })
|
|
39
|
+
} catch (error) {
|
|
40
|
+
applyServerError(error, ['code', 'password'], form.setError)
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Every other session was ended; this one starts signed in.
|
|
45
|
+
await queryClient.invalidateQueries({ queryKey: sessionQuery(auth).queryKey })
|
|
46
|
+
onReset()
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<View className="gap-6">
|
|
51
|
+
<Text className="text-sm text-neutral-500">
|
|
52
|
+
If {email} has an account, a 6-digit code is on its way to it.
|
|
53
|
+
</Text>
|
|
54
|
+
<FormError errors={errors} />
|
|
55
|
+
<Controller
|
|
56
|
+
control={form.control}
|
|
57
|
+
name="code"
|
|
58
|
+
render={({ field }) => (
|
|
59
|
+
<Field
|
|
60
|
+
label="Code"
|
|
61
|
+
keyboardType="number-pad"
|
|
62
|
+
autoComplete="one-time-code"
|
|
63
|
+
textContentType="oneTimeCode"
|
|
64
|
+
value={field.value}
|
|
65
|
+
onChangeText={field.onChange}
|
|
66
|
+
onBlur={field.onBlur}
|
|
67
|
+
error={errors.code?.message}
|
|
68
|
+
/>
|
|
69
|
+
)}
|
|
70
|
+
/>
|
|
71
|
+
<Controller
|
|
72
|
+
control={form.control}
|
|
73
|
+
name="password"
|
|
74
|
+
render={({ field }) => (
|
|
75
|
+
<Field
|
|
76
|
+
label="New password"
|
|
77
|
+
secureTextEntry
|
|
78
|
+
autoComplete="new-password"
|
|
79
|
+
value={field.value}
|
|
80
|
+
onChangeText={field.onChange}
|
|
81
|
+
onBlur={field.onBlur}
|
|
82
|
+
error={errors.password?.message}
|
|
83
|
+
/>
|
|
84
|
+
)}
|
|
85
|
+
/>
|
|
86
|
+
<Button
|
|
87
|
+
label="Set the new password"
|
|
88
|
+
disabled={isSubmitting}
|
|
89
|
+
onPress={form.handleSubmit(onSubmit)}
|
|
90
|
+
/>
|
|
91
|
+
</View>
|
|
92
|
+
)
|
|
93
|
+
}
|
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
import { zodResolver } from '@hookform/resolvers/zod'
|
|
2
2
|
import { useQueryClient } from '@tanstack/react-query'
|
|
3
|
+
import { useRouter } from 'expo-router' // prumo:email
|
|
3
4
|
import { Controller, useForm } from 'react-hook-form'
|
|
4
5
|
import { View } from 'react-native'
|
|
5
6
|
import { z } from 'zod'
|
|
6
|
-
import {
|
|
7
|
+
import { type AuthClient, sessionQuery } from '@/api-contract'
|
|
7
8
|
import { Button } from '@/components/ui/button'
|
|
8
9
|
import { Field } from '@/components/ui/field'
|
|
9
10
|
import { FormError } from '@/features/forms/form-error'
|
|
10
11
|
import { applyServerError } from '@/features/forms/server-errors'
|
|
11
|
-
import type { SessionAuth } from './auth-client'
|
|
12
|
-
import { sessionQuery } from './session'
|
|
13
12
|
|
|
14
13
|
const schema = z.object({
|
|
15
14
|
email: z.email(),
|
|
@@ -18,8 +17,15 @@ const schema = z.object({
|
|
|
18
17
|
|
|
19
18
|
type SignInValues = z.infer<typeof schema>
|
|
20
19
|
|
|
21
|
-
export function SignInForm({
|
|
20
|
+
export function SignInForm({
|
|
21
|
+
auth,
|
|
22
|
+
onSignedIn,
|
|
23
|
+
}: {
|
|
24
|
+
auth: Pick<AuthClient, 'signIn' | 'getSession'>
|
|
25
|
+
onSignedIn: () => void
|
|
26
|
+
}) {
|
|
22
27
|
const queryClient = useQueryClient()
|
|
28
|
+
const router = useRouter() // prumo:email
|
|
23
29
|
const form = useForm<SignInValues>({
|
|
24
30
|
resolver: zodResolver(schema),
|
|
25
31
|
mode: 'onTouched',
|
|
@@ -29,10 +35,18 @@ export function SignInForm({ auth, onSignedIn }: { auth: SessionAuth; onSignedIn
|
|
|
29
35
|
const { errors, isSubmitting } = form.formState
|
|
30
36
|
|
|
31
37
|
async function onSubmit(values: SignInValues) {
|
|
32
|
-
|
|
38
|
+
try {
|
|
39
|
+
await auth.signIn(values)
|
|
40
|
+
} catch (error) {
|
|
41
|
+
// prumo:email
|
|
42
|
+
// The password was right and the email is not confirmed yet: the code screen offers a new code.
|
|
43
|
+
if ((error as { code?: string }).code === 'email_not_verified') {
|
|
44
|
+
router.push({ pathname: '/verify-email', params: { email: values.email } })
|
|
45
|
+
return
|
|
46
|
+
}
|
|
33
47
|
|
|
34
|
-
|
|
35
|
-
applyServerError(
|
|
48
|
+
// prumo:end-email
|
|
49
|
+
applyServerError(error, ['email', 'password'], form.setError)
|
|
36
50
|
return
|
|
37
51
|
}
|
|
38
52
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useQueryClient } from '@tanstack/react-query'
|
|
2
|
+
import type { AuthClient } from '@/api-contract'
|
|
2
3
|
import { Button } from '@/components/ui/button'
|
|
3
4
|
import { clearUserData } from '@/features/storage/storage'
|
|
4
|
-
import type { AuthClient } from './auth-client'
|
|
5
5
|
|
|
6
6
|
export type SignOutAuth = Pick<AuthClient, 'signOut'>
|
|
7
7
|
|
|
@@ -18,11 +18,11 @@ export function SignOutButton({
|
|
|
18
18
|
try {
|
|
19
19
|
await auth.signOut()
|
|
20
20
|
} catch {
|
|
21
|
-
// A failed request leaves nothing on the device to act on: the
|
|
22
|
-
//
|
|
21
|
+
// A failed request leaves nothing on the device to act on: the auth client forgets the tokens either way. The
|
|
22
|
+
// server's session outlives them until it expires, and signing out locally is still what was asked.
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
// What the
|
|
25
|
+
// What the auth client cannot know is the rest of this user's data: the query cache and every user-scoped key, or the
|
|
26
26
|
// next person on this device reads them.
|
|
27
27
|
queryClient.clear()
|
|
28
28
|
clearUserData()
|
|
@@ -1,26 +1,32 @@
|
|
|
1
1
|
import { zodResolver } from '@hookform/resolvers/zod'
|
|
2
2
|
import { useQueryClient } from '@tanstack/react-query'
|
|
3
|
+
import { useRouter } from 'expo-router' // prumo:email
|
|
3
4
|
import { Controller, useForm } from 'react-hook-form'
|
|
4
5
|
import { View } from 'react-native'
|
|
5
6
|
import { z } from 'zod'
|
|
6
|
-
import {
|
|
7
|
+
import { type AuthClient, sessionQuery } from '@/api-contract'
|
|
7
8
|
import { Button } from '@/components/ui/button'
|
|
8
9
|
import { Field } from '@/components/ui/field'
|
|
9
10
|
import { FormError } from '@/features/forms/form-error'
|
|
10
11
|
import { applyServerError } from '@/features/forms/server-errors'
|
|
11
|
-
import type { SessionAuth } from './auth-client'
|
|
12
|
-
import { sessionQuery } from './session'
|
|
13
12
|
|
|
14
13
|
const schema = z.object({
|
|
15
14
|
name: z.string().min(1).max(80),
|
|
16
15
|
email: z.email(),
|
|
17
|
-
password: z.string().min(8),
|
|
16
|
+
password: z.string().min(8).max(128),
|
|
18
17
|
})
|
|
19
18
|
|
|
20
19
|
type SignUpValues = z.infer<typeof schema>
|
|
21
20
|
|
|
22
|
-
export function SignUpForm({
|
|
21
|
+
export function SignUpForm({
|
|
22
|
+
auth,
|
|
23
|
+
onSignedUp,
|
|
24
|
+
}: {
|
|
25
|
+
auth: Pick<AuthClient, 'signUp' | 'getSession'>
|
|
26
|
+
onSignedUp: () => void
|
|
27
|
+
}) {
|
|
23
28
|
const queryClient = useQueryClient()
|
|
29
|
+
const router = useRouter() // prumo:email
|
|
24
30
|
const form = useForm<SignUpValues>({
|
|
25
31
|
resolver: zodResolver(schema),
|
|
26
32
|
mode: 'onTouched',
|
|
@@ -30,13 +36,23 @@ export function SignUpForm({ auth, onSignedUp }: { auth: SessionAuth; onSignedUp
|
|
|
30
36
|
const { errors, isSubmitting } = form.formState
|
|
31
37
|
|
|
32
38
|
async function onSubmit(values: SignUpValues) {
|
|
33
|
-
const
|
|
39
|
+
const result = await auth.signUp(values).catch((error: unknown) => {
|
|
40
|
+
applyServerError(error, ['name', 'email', 'password'], form.setError)
|
|
41
|
+
return undefined
|
|
42
|
+
})
|
|
34
43
|
|
|
35
|
-
if (
|
|
36
|
-
applyServerError(ApiError.fromAuthError(error), ['name', 'email', 'password'], form.setError)
|
|
44
|
+
if (result === undefined) {
|
|
37
45
|
return
|
|
38
46
|
}
|
|
39
47
|
|
|
48
|
+
// prumo:email
|
|
49
|
+
if (result.verificationRequired) {
|
|
50
|
+
router.push({ pathname: '/verify-email', params: { email: values.email } })
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// prumo:end-email
|
|
55
|
+
|
|
40
56
|
await queryClient.invalidateQueries({ queryKey: sessionQuery(auth).queryKey })
|
|
41
57
|
onSignedUp()
|
|
42
58
|
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { useQueryClient } from '@tanstack/react-query'
|
|
2
|
+
import * as Linking from 'expo-linking'
|
|
3
|
+
import * as WebBrowser from 'expo-web-browser'
|
|
4
|
+
import { useState } from 'react'
|
|
5
|
+
import { Text, View } from 'react-native'
|
|
6
|
+
import { type AuthClient, type SocialProvider, sessionQuery } from '@/api-contract'
|
|
7
|
+
import { Button } from '@/components/ui/button'
|
|
8
|
+
|
|
9
|
+
// Read by hand: the return URL carries one parameter, and parsing it needs nothing from the platform.
|
|
10
|
+
function codeFrom(url: string): string | undefined {
|
|
11
|
+
for (const pair of (url.split('?')[1] ?? '').split('&')) {
|
|
12
|
+
const [key, value] = pair.split('=')
|
|
13
|
+
|
|
14
|
+
if (key === 'code' && value !== undefined && value !== '') {
|
|
15
|
+
return decodeURIComponent(value)
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return undefined
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// The system browser signs in at the provider and comes back to this app's scheme with a one-time code; the code,
|
|
23
|
+
// not a token, is what travels in the URL, and the app trades it for its tokens.
|
|
24
|
+
export function SocialButtons({
|
|
25
|
+
auth,
|
|
26
|
+
onSignedIn,
|
|
27
|
+
}: {
|
|
28
|
+
auth: Pick<AuthClient, 'socialSignInUrl' | 'exchangeSocialCode' | 'getSession'>
|
|
29
|
+
onSignedIn: () => void
|
|
30
|
+
}) {
|
|
31
|
+
const queryClient = useQueryClient()
|
|
32
|
+
const [failed, setFailed] = useState(false)
|
|
33
|
+
|
|
34
|
+
async function signInWith(provider: SocialProvider) {
|
|
35
|
+
setFailed(false)
|
|
36
|
+
|
|
37
|
+
const result = await WebBrowser.openAuthSessionAsync(
|
|
38
|
+
auth.socialSignInUrl(provider, 'mobile'),
|
|
39
|
+
Linking.createURL('social'),
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
// Closing the browser is a decision, not a failure.
|
|
43
|
+
if (result.type !== 'success') {
|
|
44
|
+
return
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const code = codeFrom(result.url)
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
if (code === undefined) {
|
|
51
|
+
throw new Error('The provider sign-in came back without a code')
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
await auth.exchangeSocialCode(code)
|
|
55
|
+
} catch {
|
|
56
|
+
setFailed(true)
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
await queryClient.invalidateQueries({ queryKey: sessionQuery(auth).queryKey })
|
|
61
|
+
onSignedIn()
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<View className="gap-3">
|
|
66
|
+
{failed ? (
|
|
67
|
+
<Text accessibilityRole="alert" className="text-sm text-red-600">
|
|
68
|
+
The sign-in did not complete. Try again.
|
|
69
|
+
</Text>
|
|
70
|
+
) : null}
|
|
71
|
+
{/* prumo:google */}
|
|
72
|
+
<Button label="Continue with Google" variant="outline" onPress={() => signInWith('google')} />
|
|
73
|
+
{/* prumo:end-google */}
|
|
74
|
+
{/* prumo:apple */}
|
|
75
|
+
<Button label="Continue with Apple" variant="outline" onPress={() => signInWith('apple')} />
|
|
76
|
+
{/* prumo:end-apple */}
|
|
77
|
+
</View>
|
|
78
|
+
)
|
|
79
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import * as SecureStore from 'expo-secure-store'
|
|
2
|
+
import type { TokenStore, Tokens } from '@/api-contract'
|
|
3
|
+
|
|
4
|
+
const KEY = 'auth.tokens'
|
|
5
|
+
|
|
6
|
+
// The Keychain on iOS and the Keystore on Android: the tokens never sit in plain storage, MMKV included.
|
|
7
|
+
export const secureTokenStore: TokenStore = {
|
|
8
|
+
async read() {
|
|
9
|
+
const value = await SecureStore.getItemAsync(KEY)
|
|
10
|
+
|
|
11
|
+
return value === null ? null : (JSON.parse(value) as Tokens)
|
|
12
|
+
},
|
|
13
|
+
|
|
14
|
+
async write(tokens) {
|
|
15
|
+
if (tokens === null) {
|
|
16
|
+
await SecureStore.deleteItemAsync(KEY)
|
|
17
|
+
} else {
|
|
18
|
+
await SecureStore.setItemAsync(KEY, JSON.stringify(tokens))
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { zodResolver } from '@hookform/resolvers/zod'
|
|
2
|
+
import { useQueryClient } from '@tanstack/react-query'
|
|
3
|
+
import { useState } from 'react'
|
|
4
|
+
import { Controller, useForm } from 'react-hook-form'
|
|
5
|
+
import { Text, View } from 'react-native'
|
|
6
|
+
import { z } from 'zod'
|
|
7
|
+
import { type AuthClient, sessionQuery } from '@/api-contract'
|
|
8
|
+
import { Button } from '@/components/ui/button'
|
|
9
|
+
import { Field } from '@/components/ui/field'
|
|
10
|
+
import { FormError } from '@/features/forms/form-error'
|
|
11
|
+
import { applyServerError } from '@/features/forms/server-errors'
|
|
12
|
+
|
|
13
|
+
const schema = z.object({ code: z.string().regex(/^\d{6}$/, 'Enter the 6 digits from the email') })
|
|
14
|
+
|
|
15
|
+
type VerifyEmailValues = z.infer<typeof schema>
|
|
16
|
+
|
|
17
|
+
export function VerifyEmailForm({
|
|
18
|
+
auth,
|
|
19
|
+
email,
|
|
20
|
+
onVerified,
|
|
21
|
+
}: {
|
|
22
|
+
auth: Pick<AuthClient, 'verifyEmail' | 'requestEmailVerification' | 'getSession'>
|
|
23
|
+
email: string
|
|
24
|
+
onVerified: () => void
|
|
25
|
+
}) {
|
|
26
|
+
const queryClient = useQueryClient()
|
|
27
|
+
const [resent, setResent] = useState(false)
|
|
28
|
+
const form = useForm<VerifyEmailValues>({
|
|
29
|
+
resolver: zodResolver(schema),
|
|
30
|
+
mode: 'onTouched',
|
|
31
|
+
defaultValues: { code: '' },
|
|
32
|
+
})
|
|
33
|
+
const { errors, isSubmitting } = form.formState
|
|
34
|
+
|
|
35
|
+
async function onSubmit(values: VerifyEmailValues) {
|
|
36
|
+
try {
|
|
37
|
+
await auth.verifyEmail({ email, code: values.code })
|
|
38
|
+
} catch (error) {
|
|
39
|
+
applyServerError(error, ['code'], form.setError)
|
|
40
|
+
return
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
await queryClient.invalidateQueries({ queryKey: sessionQuery(auth).queryKey })
|
|
44
|
+
onVerified()
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function resend() {
|
|
48
|
+
await auth.requestEmailVerification(email)
|
|
49
|
+
setResent(true)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<View className="gap-6">
|
|
54
|
+
<Text className="text-sm text-neutral-500">We sent a 6-digit code to {email}.</Text>
|
|
55
|
+
<FormError errors={errors} />
|
|
56
|
+
<Controller
|
|
57
|
+
control={form.control}
|
|
58
|
+
name="code"
|
|
59
|
+
render={({ field }) => (
|
|
60
|
+
<Field
|
|
61
|
+
label="Code"
|
|
62
|
+
keyboardType="number-pad"
|
|
63
|
+
autoComplete="one-time-code"
|
|
64
|
+
textContentType="oneTimeCode"
|
|
65
|
+
value={field.value}
|
|
66
|
+
onChangeText={field.onChange}
|
|
67
|
+
onBlur={field.onBlur}
|
|
68
|
+
error={errors.code?.message}
|
|
69
|
+
/>
|
|
70
|
+
)}
|
|
71
|
+
/>
|
|
72
|
+
<Button label="Confirm email" disabled={isSubmitting} onPress={form.handleSubmit(onSubmit)} />
|
|
73
|
+
<Button label="Send a new code" variant="outline" onPress={resend} />
|
|
74
|
+
{resent ? (
|
|
75
|
+
<Text accessibilityRole="text" className="text-sm text-neutral-500">
|
|
76
|
+
A new code is on its way. The previous one no longer works.
|
|
77
|
+
</Text>
|
|
78
|
+
) : null}
|
|
79
|
+
</View>
|
|
80
|
+
)
|
|
81
|
+
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { createContext, type ReactNode, useContext } from 'react'
|
|
2
|
-
import type { ApiClient } from '@/api-contract'
|
|
3
|
-
import type { AuthClient } from '@/features/auth/auth-client'
|
|
2
|
+
import type { ApiClient, AuthClient } from '@/api-contract'
|
|
4
3
|
|
|
5
4
|
type Clients = { api: ApiClient; auth: AuthClient }
|
|
6
5
|
|
|
@@ -1,26 +1,28 @@
|
|
|
1
1
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|
2
2
|
import { API_URL, fakeTransport, json } from '@test/support/fake-transport'
|
|
3
|
-
import {
|
|
3
|
+
import { memoryTokenStore } from '@test/support/memory-token-store'
|
|
4
4
|
import { renderRouter, screen } from 'expo-router/testing-library'
|
|
5
5
|
import type { ReactNode } from 'react'
|
|
6
6
|
import { Text } from 'react-native'
|
|
7
|
-
import { createClient } from '@/api-contract'
|
|
7
|
+
import { createAuthClient, createClient } from '@/api-contract'
|
|
8
8
|
import AuthenticatedLayout from '@/app/(app)/_layout'
|
|
9
|
-
import type { AuthClient } from '@/features/auth/auth-client'
|
|
10
9
|
import { ClientsProvider } from '@/features/clients/clients-context'
|
|
11
10
|
|
|
12
|
-
const user = { id: 'user-1', email: 'ana@example.com'
|
|
11
|
+
const user = { id: 'user-1', email: 'ana@example.com' }
|
|
12
|
+
|
|
13
|
+
const TOKENS = { accessToken: 'access-1', refreshToken: 'refresh-1', expiresIn: 900 }
|
|
13
14
|
|
|
14
15
|
async function renderAt(getSession: () => Response | Promise<Response>) {
|
|
15
|
-
const transport = fakeTransport({
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
const transport = fakeTransport({
|
|
17
|
+
'GET /api/auth/session': getSession,
|
|
18
|
+
'POST /api/auth/refresh': () => json(401, {}),
|
|
19
|
+
})
|
|
18
20
|
const auth = createAuthClient({
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
})
|
|
23
|
-
const clients = { auth, api: createClient({ baseUrl: API_URL, fetch:
|
|
21
|
+
baseUrl: API_URL,
|
|
22
|
+
fetch: transport,
|
|
23
|
+
tokens: memoryTokenStore(TOKENS),
|
|
24
|
+
})
|
|
25
|
+
const clients = { auth, api: createClient({ baseUrl: API_URL, fetch: auth.fetch }) }
|
|
24
26
|
const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } })
|
|
25
27
|
|
|
26
28
|
function Providers({ children }: { children: ReactNode }) {
|
|
@@ -46,7 +48,7 @@ describe('AuthenticatedLayout', () => {
|
|
|
46
48
|
it('waits for a session still on its way instead of sending a signed-in visitor to sign in', async () => {
|
|
47
49
|
await renderAt(async () => {
|
|
48
50
|
await new Promise((resolve) => setTimeout(resolve, 50))
|
|
49
|
-
return json(200, {
|
|
51
|
+
return json(200, { user })
|
|
50
52
|
})
|
|
51
53
|
|
|
52
54
|
expect(await screen.findByText('Your profile')).toBeOnTheScreen()
|
|
@@ -54,7 +56,7 @@ describe('AuthenticatedLayout', () => {
|
|
|
54
56
|
})
|
|
55
57
|
|
|
56
58
|
it('sends a visitor without a session to sign in', async () => {
|
|
57
|
-
await renderAt(() => json(
|
|
59
|
+
await renderAt(() => json(401, {}))
|
|
58
60
|
|
|
59
61
|
expect(await screen.findByText('Sign in')).toBeOnTheScreen()
|
|
60
62
|
})
|