diff --git a/packages/keychain/src/components/app.tsx b/packages/keychain/src/components/app.tsx index f469bfbec..9214fb53b 100644 --- a/packages/keychain/src/components/app.tsx +++ b/packages/keychain/src/components/app.tsx @@ -31,6 +31,7 @@ import { AddSignerRoute } from "./settings/AddSignerRoute"; import { AddConnectionRoute } from "./settings/AddConnectionRoute"; import { PaymentMethod } from "./purchasenew/method"; import { Verification } from "./purchasenew/verification"; +import { StripeVerification } from "./purchasenew/verification/stripe"; import { StripeCheckout } from "./purchasenew/checkout/stripe"; import { Success as PurchaseSuccess } from "./purchasenew/success"; import { Pending as PurchasePending } from "./purchasenew/pending"; @@ -271,6 +272,7 @@ export function App() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/packages/keychain/src/components/purchasenew/checkout/onchain/index.tsx b/packages/keychain/src/components/purchasenew/checkout/onchain/index.tsx index 2e612619a..06a7dfdfb 100644 --- a/packages/keychain/src/components/purchasenew/checkout/onchain/index.tsx +++ b/packages/keychain/src/components/purchasenew/checkout/onchain/index.tsx @@ -224,7 +224,16 @@ export function OnchainCheckout() { try { if (isStripeSelected) { await onCreditCardPurchase(); - navigate("/purchase/checkout/stripe"); + + const { data: accountPrivateData } = await refetchAccountPrivate(); + const needsVerification = + !accountPrivateData?.accountPrivate?.proveVerifiedAt; + + if (needsVerification) { + navigate("/purchase/verification/stripe"); + } else { + navigate("/purchase/checkout/stripe"); + } } else if (isApplePaySelected) { const [{ data: meData }, { data: accountPrivateData }] = await Promise.all([refetchMe(), refetchAccountPrivate()]); diff --git a/packages/keychain/src/components/purchasenew/verification/stripe.tsx b/packages/keychain/src/components/purchasenew/verification/stripe.tsx new file mode 100644 index 000000000..70a2f7ca1 --- /dev/null +++ b/packages/keychain/src/components/purchasenew/verification/stripe.tsx @@ -0,0 +1,219 @@ +import { useState, useEffect } from "react"; +import { + Button, + Input, + CheckIcon, + Thumbnail, + CreditCardIcon, + HeaderInner, + LayoutContent, + LayoutFooter, + Card, + CardContent, + UserIcon, +} from "@cartridge/ui"; +import { useNavigation } from "@/context"; +import { ErrorAlert } from "@/components/ErrorAlert"; +import { useAccountVerifyMutation, useAccountPrivateQuery } from "@/utils/api"; +import { useConnection } from "@/hooks/connection"; + +export function StripeVerification() { + const { navigate, setShowClose } = useNavigation(); + const { isMainnet } = useConnection(); + const accountVerifyMutation = useAccountVerifyMutation(); + const { refetch: refetchAccountPrivate } = useAccountPrivateQuery(undefined, { + enabled: false, + }); + + const [firstName, setFirstName] = useState(""); + const [lastName, setLastName] = useState(""); + const [phone, setPhone] = useState(""); + const [error, setError] = useState(null); + const [isSuccess, setIsSuccess] = useState(false); + + useEffect(() => { + setShowClose(true); + }, [setShowClose]); + + useEffect(() => { + if (isSuccess) { + const timer = setTimeout(() => { + navigate("/purchase/checkout/stripe"); + }, 1500); + return () => clearTimeout(timer); + } + }, [isSuccess, navigate]); + + const handleSubmit = async () => { + setError(null); + + if (!firstName.trim()) { + setError("Please enter your first name."); + return; + } + if (!lastName.trim()) { + setError("Please enter your last name."); + return; + } + + const cleanPhone = phone.replace(/\D/g, ""); + let formattedPhone = ""; + + if (cleanPhone.length === 10) { + formattedPhone = `+1${cleanPhone}`; + } else if (cleanPhone.length === 11 && cleanPhone.startsWith("1")) { + formattedPhone = `+${cleanPhone}`; + } else { + setError("Please enter a valid 10-digit US phone number."); + return; + } + + try { + const res = await accountVerifyMutation.mutateAsync({ + input: { + firstName: firstName.trim(), + lastName: lastName.trim(), + phoneNumber: formattedPhone, + sandbox: !isMainnet, + }, + }); + + if (res.accountVerify) { + await refetchAccountPrivate(); + setIsSuccess(true); + } else { + setError("Verification failed."); + } + } catch (e: unknown) { + setError(e instanceof Error ? e.message : "Verification failed."); + } + }; + + if (isSuccess) { + return ( + <> + } + variant="compressed" + /> + + + + } + size="lg" + className="bg-background-300" + rounded + /> + + Verified! + + Continuing to payment... + + + + + + + > + ); + } + + return ( + <> + } + variant="compressed" + /> + + + + + First Name + + ) => { + setFirstName(e.target.value); + setError(null); + }} + onKeyDown={(e: React.KeyboardEvent) => + e.key === "Enter" && + firstName && + lastName && + phone && + handleSubmit() + } + type="text" + /> + + + + Last Name + + ) => { + setLastName(e.target.value); + setError(null); + }} + onKeyDown={(e: React.KeyboardEvent) => + e.key === "Enter" && + firstName && + lastName && + phone && + handleSubmit() + } + type="text" + /> + + + + Phone Number + + ) => { + setPhone(e.target.value); + setError(null); + }} + onKeyDown={(e: React.KeyboardEvent) => + e.key === "Enter" && + firstName && + lastName && + phone && + handleSubmit() + } + type="tel" + /> + + + + + {error && ( + + )} + + CONTINUE + + + > + ); +} diff --git a/packages/keychain/src/utils/api/account.graphql b/packages/keychain/src/utils/api/account.graphql index 021fee07b..9bfe5c3a5 100644 --- a/packages/keychain/src/utils/api/account.graphql +++ b/packages/keychain/src/utils/api/account.graphql @@ -90,3 +90,7 @@ query AccountSearch($query: String!, $limit: Int = 5) { updatedAt } } + +mutation AccountVerify($input: AccountVerifyInput!) { + accountVerify(input: $input) +} diff --git a/packages/keychain/src/utils/api/generated.ts b/packages/keychain/src/utils/api/generated.ts index c6f3e3075..ee84ba799 100644 --- a/packages/keychain/src/utils/api/generated.ts +++ b/packages/keychain/src/utils/api/generated.ts @@ -215,6 +215,15 @@ export type AccountUpdateInput = { email?: InputMaybe; }; +export type AccountVerifyInput = { + emailAddress?: InputMaybe; + firstName?: InputMaybe; + lastName?: InputMaybe; + phoneNumber?: InputMaybe; + /** Use the UAT sandbox environment instead of production. */ + sandbox?: InputMaybe; +}; + /** * AccountWhereInput is used for filtering Account objects. * Input was generated by ent. @@ -2960,6 +2969,7 @@ export type MintAllowance = { export type Mutation = { __typename?: "Mutation"; + accountVerify: Scalars["Boolean"]; addOwner: Scalars["Boolean"]; addPolicies?: Maybe>; addToTeam: Scalars["Boolean"]; @@ -2998,11 +3008,6 @@ export type Mutation = { finalizeLogin: Scalars["String"]; finalizeRegistration: Account; increaseBudget: Paymaster; - /** - * Perform Prove Identity Verify v2. - * Submits consumer PII for verification. - */ - proveVerify: ProveVerifyResponse; register: Account; removeAllPolicies: Scalars["Boolean"]; removeFromTeam: Scalars["Boolean"]; @@ -3041,6 +3046,10 @@ export type Mutation = { verifyPhone: VerifyResponse; }; +export type MutationAccountVerifyArgs = { + input: AccountVerifyInput; +}; + export type MutationAddOwnerArgs = { chainID: Scalars["String"]; owner: SignerInput; @@ -3200,10 +3209,6 @@ export type MutationIncreaseBudgetArgs = { unit: FeeUnit; }; -export type MutationProveVerifyArgs = { - input: ProveVerifyInput; -}; - export type MutationRegisterArgs = { chainId: Scalars["String"]; owner: SignerInput; @@ -4360,77 +4365,6 @@ export type Project = { project: Scalars["String"]; }; -export type ProveAddressResult = { - __typename?: "ProveAddressResult"; - addressScore: Scalars["Int"]; - city: Scalars["Boolean"]; - distance: Scalars["Float"]; - postalCode: Scalars["Boolean"]; - region: Scalars["Boolean"]; - street: Scalars["Boolean"]; - streetNumber: Scalars["Int"]; -}; - -export type ProveEmailResult = { - __typename?: "ProveEmailResult"; - emailAddress: Scalars["Boolean"]; -}; - -export type ProveIdentifiersResult = { - __typename?: "ProveIdentifiersResult"; - dob: Scalars["Boolean"]; - driversLicenseNumber: Scalars["Boolean"]; - driversLicenseState: Scalars["Boolean"]; - last4: Scalars["Boolean"]; - ssn: Scalars["Boolean"]; -}; - -export type ProveNameResult = { - __typename?: "ProveNameResult"; - firstName: Scalars["Int"]; - lastName: Scalars["Int"]; - nameScore: Scalars["Int"]; -}; - -export type ProveVerifyInput = { - address?: InputMaybe; - city?: InputMaybe; - dob?: InputMaybe; - emailAddress?: InputMaybe; - extendedAddress?: InputMaybe; - /** Use the UAT sandbox environment instead of production. */ - firstName?: InputMaybe; - last4?: InputMaybe; - lastName?: InputMaybe; - phoneNumber?: InputMaybe; - postalCode?: InputMaybe; - region?: InputMaybe; - ssn?: InputMaybe; -}; - -export type ProveVerifyResponse = { - __typename?: "ProveVerifyResponse"; - address?: Maybe; - carrier?: Maybe; - cipConfidence?: Maybe; - countryCode?: Maybe; - description: Scalars["String"]; - email?: Maybe; - enrollStatus?: Maybe; - identifiers?: Maybe; - lineType?: Maybe; - multiCipConfidence?: Maybe; - multiVerified?: Maybe; - name?: Maybe; - payfoneAlias?: Maybe; - phoneNumber?: Maybe; - reasonCodes?: Maybe>; - status: Scalars["Int"]; - success: Scalars["Boolean"]; - transactionId?: Maybe; - verified?: Maybe; -}; - export type PurchaseFulfillment = { __typename?: "PurchaseFulfillment"; id: Scalars["ID"]; @@ -7195,6 +7129,15 @@ export type AccountSearchQuery = { }>; }; +export type AccountVerifyMutationVariables = Exact<{ + input: AccountVerifyInput; +}>; + +export type AccountVerifyMutation = { + __typename?: "Mutation"; + accountVerify: boolean; +}; + export type CryptoPaymentQueryVariables = Exact<{ id: Scalars["ID"]; }>; @@ -7871,6 +7814,31 @@ export const useAccountSearchQuery = < ).bind(null, variables), options, ); +export const AccountVerifyDocument = ` + mutation AccountVerify($input: AccountVerifyInput!) { + accountVerify(input: $input) +} + `; +export const useAccountVerifyMutation = ( + options?: UseMutationOptions< + AccountVerifyMutation, + TError, + AccountVerifyMutationVariables, + TContext + >, +) => + useMutation< + AccountVerifyMutation, + TError, + AccountVerifyMutationVariables, + TContext + >( + ["AccountVerify"], + useFetchData( + AccountVerifyDocument, + ), + options, + ); export const CryptoPaymentDocument = ` query CryptoPayment($id: ID!) { cryptoPayment(id: $id) {
+ Continuing to payment... +