diff --git a/apps/app/public/locales/en/common.json b/apps/app/public/locales/en/common.json
index 44f81f944e..358796651c 100644
--- a/apps/app/public/locales/en/common.json
+++ b/apps/app/public/locales/en/common.json
@@ -53,6 +53,8 @@
"close": "Close",
"confirm-account": {
"click-verify": "Click here to verify your confirm your InReach account",
+ "code-requested": "Verification code requested!",
+ "code-resent": "A new code has been requested. Please check your email.",
"message": "Click the following link to confirm your account:",
"subject": "Confirm your account"
},
@@ -114,7 +116,10 @@
"404-title": "404: Page not found.",
"500-body": "We're sorry, something went wrong with our server. Please try again later, or start a search below to find safe, verified LGBTQ+ resources in your area.",
"500-title": "500: Something went wrong.",
+ "code-expired": "The code provided has expired.",
+ "code-mismatch": "The code provided is not valid.",
"oh-no": "Oh no!",
+ "resend-code": "Request a new code",
"try-again-text": "Something went wrong! Please try again."
},
"exclude": "Exclude",
diff --git a/lambdas/cognito-messaging/samconfig.toml b/lambdas/cognito-messaging/samconfig.toml
index c248d9ce6c..1eee0fde4c 100644
--- a/lambdas/cognito-messaging/samconfig.toml
+++ b/lambdas/cognito-messaging/samconfig.toml
@@ -7,3 +7,5 @@ region = "us-east-1"
confirm_changeset = true
capabilities = "CAPABILITY_IAM"
image_repositories = []
+[default.build.parameters]
+build_in_source = true
diff --git a/lambdas/cognito-user-migrate/samconfig.toml b/lambdas/cognito-user-migrate/samconfig.toml
index 90025a6f6a..09c01f2092 100644
--- a/lambdas/cognito-user-migrate/samconfig.toml
+++ b/lambdas/cognito-user-migrate/samconfig.toml
@@ -7,3 +7,5 @@ region = "us-east-1"
confirm_changeset = true
capabilities = "CAPABILITY_IAM"
image_repositories = []
+[default.build.parameters]
+build_in_source = true
diff --git a/packages/api/router/user/index.ts b/packages/api/router/user/index.ts
index 68580cc556..274f0cbf79 100644
--- a/packages/api/router/user/index.ts
+++ b/packages/api/router/user/index.ts
@@ -91,4 +91,11 @@ export const userRouter = defineRouter({
)
return handler({ input, ctx })
}),
+ resendCode: publicProcedure.input(schema.ZResendCodeSchema).mutation(async (opts) => {
+ const handler = await importHandler(
+ namespaced('resendCode'),
+ () => import('./mutation.resendCode.handler')
+ )
+ return handler(opts)
+ }),
})
diff --git a/packages/api/router/user/mutation.confirmAccount.handler.ts b/packages/api/router/user/mutation.confirmAccount.handler.ts
index c1f0e1a30f..e77a287549 100644
--- a/packages/api/router/user/mutation.confirmAccount.handler.ts
+++ b/packages/api/router/user/mutation.confirmAccount.handler.ts
@@ -1,33 +1,49 @@
-import { confirmAccount as cognitoConfirmAccount } from '@weareinreach/auth/confirmAccount'
+import { TRPCError } from '@trpc/server'
+
+import {
+ CodeMismatchException,
+ confirmAccount as cognitoConfirmAccount,
+ ExpiredCodeException,
+} from '@weareinreach/auth/confirmAccount'
import { prisma } from '@weareinreach/db'
import { type TRPCHandlerParams } from '~api/types/handler'
import { type TConfirmAccountSchema } from './mutation.confirmAccount.schema'
const confirmAccount = async ({ input }: TRPCHandlerParams) => {
- const { code, email } = input
- const response = await cognitoConfirmAccount(email, code)
+ try {
+ const { code, email } = input
+ const response = await cognitoConfirmAccount(email, code)
- const { id } = await prisma.user.findFirstOrThrow({
- where: {
- email: {
- equals: email.toLowerCase(),
- mode: 'insensitive',
+ const { id } = await prisma.user.findFirstOrThrow({
+ where: {
+ email: {
+ equals: email.toLowerCase(),
+ mode: 'insensitive',
+ },
+ },
+ select: {
+ id: true,
},
- },
- select: {
- id: true,
- },
- })
+ })
- await prisma.user.update({
- where: {
- id,
- },
- data: {
- emailVerified: new Date(),
- },
- })
- return response
+ await prisma.user.update({
+ where: {
+ id,
+ },
+ data: {
+ emailVerified: new Date(),
+ },
+ })
+ return response
+ } catch (error) {
+ if (error instanceof CodeMismatchException) {
+ throw new TRPCError({ code: 'BAD_REQUEST', message: 'Code mismatch', cause: error })
+ }
+ if (error instanceof ExpiredCodeException) {
+ throw new TRPCError({ code: 'BAD_REQUEST', message: 'Code expired', cause: error })
+ }
+ throw error
+ }
}
export default confirmAccount
diff --git a/packages/api/router/user/mutation.resendCode.handler.ts b/packages/api/router/user/mutation.resendCode.handler.ts
new file mode 100644
index 0000000000..ce6cf255e5
--- /dev/null
+++ b/packages/api/router/user/mutation.resendCode.handler.ts
@@ -0,0 +1,15 @@
+import { resendVerificationCode } from '@weareinreach/auth/lib/resendCode'
+import { handleError } from '~api/lib/errorHandler'
+import { type TRPCHandlerParams } from '~api/types/handler'
+
+import { type TResendCodeSchema } from './mutation.resendCode.schema'
+
+const resendCode = async ({ input }: TRPCHandlerParams) => {
+ try {
+ const result = await resendVerificationCode(input.email)
+ return result
+ } catch (error) {
+ return handleError(error)
+ }
+}
+export default resendCode
diff --git a/packages/api/router/user/mutation.resendCode.schema.ts b/packages/api/router/user/mutation.resendCode.schema.ts
new file mode 100644
index 0000000000..1f4b082a81
--- /dev/null
+++ b/packages/api/router/user/mutation.resendCode.schema.ts
@@ -0,0 +1,14 @@
+import { z } from 'zod'
+
+import { decodeUrl } from '~api/lib/encodeUrl'
+
+export const ZResendCodeSchema = z.union([
+ z.object({ email: z.string().email().toLowerCase(), data: z.never().optional() }),
+ z
+ .object({ data: z.string(), email: z.never().optional() })
+ .transform(({ data }) => ({ email: decodeUrl(data).email })),
+])
+
+// .object({ email: z.string().email().toLowerCase() })
+// .or(z.object({ data: z.string() }))
+export type TResendCodeSchema = z.infer
diff --git a/packages/api/router/user/schemas.ts b/packages/api/router/user/schemas.ts
index 9e9b23b60e..b8ea8beeb3 100644
--- a/packages/api/router/user/schemas.ts
+++ b/packages/api/router/user/schemas.ts
@@ -4,6 +4,7 @@ export * from './mutation.confirmAccount.schema'
export * from './mutation.create.schema'
export * from './mutation.deleteAccount.schema'
export * from './mutation.forgotPassword.schema'
+export * from './mutation.resendCode.schema'
export * from './mutation.resetPassword.schema'
export * from './mutation.submitSurvey.schema'
// codegen:end
diff --git a/packages/auth/lib/cognitoClient.ts b/packages/auth/lib/cognitoClient.ts
index e89e0e83d6..07b52c8310 100644
--- a/packages/auth/lib/cognitoClient.ts
+++ b/packages/auth/lib/cognitoClient.ts
@@ -12,7 +12,7 @@ import { z } from 'zod'
import { createHmac } from 'crypto'
import { prisma } from '@weareinreach/db'
-import { getEnv } from '@weareinreach/env'
+import { getEnv, isLocalDev } from '@weareinreach/env'
import { createLoggerInstance } from '@weareinreach/util/logger'
import { decodeCognitoIdJwt } from './cognitoJwt'
@@ -33,6 +33,8 @@ export const cognito = new CognitoIdentityProvider({
secretAccessKey: getEnv('COGNITO_SECRET'),
},
logger,
+ // eslint-disable-next-line node/no-process-env
+ ...(isLocalDev && { endpoint: process.env.COGNITO_LOCAL_ENDPOINT }),
})
export const ClientId = getEnv('COGNITO_CLIENT_ID')
diff --git a/packages/auth/lib/confirmAccount.ts b/packages/auth/lib/confirmAccount.ts
index bdd6e983d3..0f7f2516c5 100644
--- a/packages/auth/lib/confirmAccount.ts
+++ b/packages/auth/lib/confirmAccount.ts
@@ -9,3 +9,5 @@ export const confirmAccount = async (email: string, code: string) => {
})
return response
}
+
+export { ExpiredCodeException, CodeMismatchException } from '@aws-sdk/client-cognito-identity-provider'
diff --git a/packages/env/index.ts b/packages/env/index.ts
index d27a30ebf7..5885ad1040 100644
--- a/packages/env/index.ts
+++ b/packages/env/index.ts
@@ -90,5 +90,6 @@ export const env = createEnv({
export const getEnv = (envVar: T): (typeof env)[T] => env[envVar]
-export const isDev = process.env.NODE_ENV === 'development'
-export const isVercelProd = process.env.VERCEL_ENV === 'production'
+export * from './checks'
+// export const isDev = process.env.NODE_ENV === 'development'
+// export const isVercelProd = process.env.VERCEL_ENV === 'production'
diff --git a/packages/ui/modals/AccountVerified.tsx b/packages/ui/modals/AccountVerified.tsx
index 4f6cb47c00..50d943e549 100644
--- a/packages/ui/modals/AccountVerified.tsx
+++ b/packages/ui/modals/AccountVerified.tsx
@@ -12,7 +12,7 @@ import {
import { useDisclosure } from '@mantine/hooks'
import { useRouter } from 'next/router'
import { Trans, useTranslation } from 'next-i18next'
-import { forwardRef, useEffect, useMemo, useState } from 'react'
+import { forwardRef, useCallback, useEffect, useMemo, useState } from 'react'
import { z } from 'zod'
import { decodeUrl } from '@weareinreach/api/lib/encodeUrl'
@@ -44,10 +44,27 @@ const AccountVerifyModalBody = forwardRef setSuccess(true),
onError: () => setError(true),
})
+ const resendCode = api.user.resendCode.useMutation({
+ onSuccess: () => setCodeSent(true),
+ })
+
+ const handleResendCode = useCallback((data: string) => () => resendCode.mutate({ data }), [resendCode])
+
+ const hasError = useMemo(() => {
+ if (!verifyAccount.isError) {
+ return false
+ }
+ if (verifyAccount.error.data?.cause instanceof Error && verifyAccount.error.data?.cause?.name) {
+ return verifyAccount.error.data.cause.name
+ }
+ return 'UnknownError'
+ }, [verifyAccount.error?.data?.cause, verifyAccount.isError])
+
// const DataSchema = z.string().default('')
const [opened, handler] = useDisclosure(autoOpen)
const { isMobile } = useScreenSize()
@@ -137,19 +154,52 @@ const AccountVerifyModalBody = forwardRef (
+
+ const errorI18nKey = useMemo(() => {
+ switch (hasError) {
+ case 'NotAuthorizedException':
+ case 'CodeMismatchException': {
+ return 'errors.code-mismatch'
+ }
+ case 'ExpiredCodeException': {
+ return 'errors.code-expired'
+ }
+ default: {
+ return 'errors.try-again-text'
+ }
+ }
+ }, [hasError])
+
+ const bodyError = useMemo(() => {
+ return (
🫣
{t('errors.oh-no')}
.,
}}
/>
+ {errorI18nKey !== 'errors.try-again-text' && parsedData.data && (
+
+ )}
+
+ )
+ }, [errorI18nKey, t, variants, handleResendCode, parsedData.data])
+
+ const bodyCodeResent = useMemo(
+ () => (
+
+
+ 📬
+ {t('confirm-account.code-requested')}
+
+ {t('confirm-account.code-resent')}
),
[t, variants]
@@ -159,11 +209,14 @@ const AccountVerifyModalBody = forwardRef