diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 00000000..1295ee63
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,3 @@
+{
+ "editor.formatOnSave": true
+}
diff --git a/src/components/IDKitWidget/BaseWidget.tsx b/src/components/IDKitWidget/BaseWidget.tsx
index 936c1ca1..4a4e7399 100644
--- a/src/components/IDKitWidget/BaseWidget.tsx
+++ b/src/components/IDKitWidget/BaseWidget.tsx
@@ -1,5 +1,6 @@
import root from 'react-shadow'
import { FC, useMemo } from 'react'
+import { IDKITStage } from '@/types'
import builtStyles from '@build/index.css'
import ErrorState from './States/ErrorState'
import SuccessState from './States/SuccessState'
@@ -10,7 +11,7 @@ import EnterPhoneState from './States/EnterPhoneState'
import VerifyCodeState from './States/VerifyCodeState'
import { AnimatePresence, motion } from 'framer-motion'
import QuestionMarkIcon from '../Icons/QuestionMarkIcon'
-import useIDKitStore, { IDKITStage, IDKitStore } from '@/store/idkit'
+import useIDKitStore, { IDKitStore } from '@/store/idkit'
import { ArrowLongLeftIcon, XMarkIcon } from '@heroicons/react/20/solid'
const getParams = ({ open, onOpenChange, stage, setStage }: IDKitStore) => ({
diff --git a/src/components/IDKitWidget/States/EnterPhoneState.tsx b/src/components/IDKitWidget/States/EnterPhoneState.tsx
index ed746889..1935d099 100644
--- a/src/components/IDKitWidget/States/EnterPhoneState.tsx
+++ b/src/components/IDKitWidget/States/EnterPhoneState.tsx
@@ -1,27 +1,48 @@
+import { IDKITStage } from '@/types'
import { motion } from 'framer-motion'
import PhoneInput from '@/components/PhoneInput'
import WorldIDIcon from '@/components/WorldIDIcon'
-import useIDKitStore, { IDKITStage, IDKitStore } from '@/store/idkit'
+import useIDKitStore, { IDKitStore } from '@/store/idkit'
+import { requestCode, isRequestCodeError } from '@/services/phone'
-const getParams = ({ phoneNumber, setStage }: IDKitStore) => ({
+const getParams = ({ processing, phoneNumber, actionId, setStage, setProcessing }: IDKitStore) => ({
+ processing,
phoneNumber,
+ actionId,
useWorldID: () => setStage(IDKITStage.WORLD_ID),
- onSubmit: () => setStage(IDKITStage.ENTER_CODE),
+ onSubmit: async () => {
+ try {
+ setProcessing(true)
+ // FIXME: ph_distinct_id
+ await requestCode(phoneNumber, actionId, '')
+ setProcessing(false)
+ setStage(IDKITStage.ENTER_CODE)
+ } catch (error) {
+ setProcessing(false)
+ if (isRequestCodeError(error) && error.code !== 'server_error') {
+ // FIXME: Error toast here
+ console.error(error)
+ } else {
+ setStage(IDKITStage.ERROR)
+ }
+ }
+ },
})
const EnterPhoneState = () => {
- const { phoneNumber, useWorldID, onSubmit } = useIDKitStore(getParams)
+ const { phoneNumber, processing, useWorldID, onSubmit } = useIDKitStore(getParams)
return (
+ {/* TODO: Caption should be a config option */}
Verify your phone number for free gasless transactions.
We'll take care of the rest!
-
+
We'll call or text to confirm your number. No data is stored.
@@ -46,11 +67,12 @@ const EnterPhoneState = () => {
transition={{ layout: { duration: 0.15 } }}
onClick={onSubmit}
layoutId="submit-button"
- disabled={!phoneNumber}
+ disabled={!phoneNumber || processing}
className="inline-flex items-center px-8 py-3 border border-transparent font-medium rounded-full shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:bg-indigo-600"
>
+ {/* TODO: Nicer loading state */}
- Continue
+ {processing ? 'Loading ...' : 'Continue'}
diff --git a/src/components/IDKitWidget/States/SuccessState.tsx b/src/components/IDKitWidget/States/SuccessState.tsx
index 2792964c..046452c8 100644
--- a/src/components/IDKitWidget/States/SuccessState.tsx
+++ b/src/components/IDKitWidget/States/SuccessState.tsx
@@ -13,7 +13,8 @@ const SuccessState = () => {
Success! 🎉
- Your phone number is verified and access to glassless transactions is enabled.
+ {/* TODO: This caption should be a config option */}
+ Your phone number is verified and access to gasless transactions is enabled.
diff --git a/src/components/IDKitWidget/States/VerifyCodeState.tsx b/src/components/IDKitWidget/States/VerifyCodeState.tsx
index 1f0bc51c..250546c9 100644
--- a/src/components/IDKitWidget/States/VerifyCodeState.tsx
+++ b/src/components/IDKitWidget/States/VerifyCodeState.tsx
@@ -1,30 +1,54 @@
import { useRef } from 'react'
+import { IDKITStage } from '@/types'
import { motion } from 'framer-motion'
import WorldIDIcon from '@/components/WorldIDIcon'
import SMSCodeInput from '@/components/SMSCodeInput'
import ResendButton from '@/components/ResendButton'
-import useIDKitStore, { IDKITStage, IDKitStore } from '@/store/idkit'
+import useIDKitStore, { IDKitStore } from '@/store/idkit'
+import { verifyCode, isVerifyCodeError } from '@/services/phone'
-const getParams = ({ code, setStage }: IDKitStore) => ({
+const getParams = ({ processing, phoneNumber, code, actionId, setStage, setProcessing, setCode }: IDKitStore) => ({
+ processing,
+ phoneNumber,
code,
- onSubmit: () => setStage(IDKITStage.SUCCESS),
+ actionId,
+ setCode,
+ onSubmit: async () => {
+ try {
+ setProcessing(true)
+ // FIXME: Add ph_distinct_id
+ await verifyCode(phoneNumber, code, actionId, '')
+ setProcessing(false)
+ setStage(IDKITStage.SUCCESS)
+ } catch (error) {
+ setProcessing(false)
+ setCode('')
+ if (isVerifyCodeError(error)) {
+ // FIXME: show error toast here
+ console.error(error)
+ } else {
+ setStage(IDKITStage.ERROR)
+ }
+ }
+ },
useWorldID: () => setStage(IDKITStage.WORLD_ID),
})
const VerifyCodeState = () => {
const submitRef = useRef(null)
- const { code, onSubmit, useWorldID } = useIDKitStore(getParams)
+ const { processing, code, onSubmit, useWorldID } = useIDKitStore(getParams)
return (
+ {/* TODO: Allow app to set this caption from settings */}
Verify your phone number for free gasless transactions.
We'll take care of the rest!
diff --git a/src/components/PhoneInput.tsx b/src/components/PhoneInput.tsx
index 5c832882..3f39679e 100644
--- a/src/components/PhoneInput.tsx
+++ b/src/components/PhoneInput.tsx
@@ -1,11 +1,11 @@
import { phone } from 'phone'
-import { useEffect, useState } from 'react'
+import { memo, useEffect, useState } from 'react'
import CountryCodeSelect from './CountryCodeSelect'
import useIDKitStore, { IDKitStore } from '@/store/idkit'
const getParams = ({ setPhoneNumber }: IDKitStore) => ({ setFullPhone: setPhoneNumber })
-const PhoneInput = () => {
+const PhoneInput = ({ disabled, onSubmit }: { disabled?: boolean; onSubmit?: () => Promise | void }) => {
const { setFullPhone } = useIDKitStore(getParams)
const [countryCode, setCountryCode] = useState('1')
const [phoneNumber, setPhoneNumber] = useState('')
@@ -40,10 +40,12 @@ const PhoneInput = () => {
placeholder="Phone number"
onChange={e => setPhoneNumber(e.target.value)}
className="block w-full rounded-md border-transparent bg-transparent pl-24 focus:ring-transparent focus:border-transparent sm:text-sm"
+ disabled={disabled}
+ onKeyDown={e => e.key === 'Enter' && onSubmit?.()}
/>
)
}
-export default PhoneInput
+export default memo(PhoneInput)
diff --git a/src/components/SMSCodeInput.tsx b/src/components/SMSCodeInput.tsx
index 89605985..08dd7f61 100644
--- a/src/components/SMSCodeInput.tsx
+++ b/src/components/SMSCodeInput.tsx
@@ -11,7 +11,7 @@ const fillValues = (value: string): Array6 => {
const getParams = ({ setCode }: IDKitStore) => ({ setCode })
-const SMSCodeInput = ({ submitRef }: { submitRef: RefObject }) => {
+const SMSCodeInput = ({ submitRef, disabled }: { submitRef: RefObject; disabled?: boolean }) => {
const { setCode } = useIDKitStore(getParams)
const inputsRefs = useMemo(() => new Array(6).fill(null).map(() => createRef()), [])
@@ -149,11 +149,13 @@ const SMSCodeInput = ({ submitRef }: { submitRef: RefObject }
value={values[i]}
inputMode="numeric"
autoComplete="one-time-code"
+ autoFocus={i === 0}
onFocus={() => onInputFocus(i)}
onPaste={event => onInputPaste(event, i)}
onChange={event => onInputChange(event, i)}
onKeyDown={event => onInputKeyDown(event, i)}
className="w-12 h-14 border-0 bg-gray-100 rounded-xl text-center"
+ disabled={disabled}
/>
))}
diff --git a/src/services/phone.ts b/src/services/phone.ts
new file mode 100644
index 00000000..64ac69e6
--- /dev/null
+++ b/src/services/phone.ts
@@ -0,0 +1,67 @@
+import { IPhoneSignal } from '@/types'
+
+const API_BASE_URL = 'https://developer.worldcoin.org/api/v1'
+
+export async function requestCode(phone_number: string, action_id: string, ph_distinct_id: string) {
+ const res = await fetch(`${API_BASE_URL}/phone/request`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ phone_number,
+ action_id,
+ channel: 'sms', // FIXME
+ ph_distinct_id,
+ }),
+ })
+ if (res.ok) {
+ return
+ }
+ throw await res.json()
+}
+
+export async function verifyCode(phone_number: string, code: string, action_id: string, ph_distinct_id: string) {
+ const res = await fetch(`${API_BASE_URL}/phone/verify`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ phone_number,
+ code,
+ action_id,
+ ph_distinct_id,
+ }),
+ })
+ if (res.ok) {
+ return (await res.json()) as IPhoneSignal
+ }
+ throw await res.json()
+}
+
+export interface RequestCodeError {
+ code: 'timeout' | 'max_attempts' | 'server_error'
+ details: string
+}
+
+export function isRequestCodeError(error: unknown): error is RequestCodeError {
+ return (
+ typeof error === 'object' &&
+ error !== null &&
+ Object.prototype.hasOwnProperty.call(error as Record, 'code')
+ )
+}
+
+export interface VerifyCodeError {
+ code: 'invalid_code'
+ details: string
+}
+
+export function isVerifyCodeError(error: unknown): error is VerifyCodeError {
+ return (
+ typeof error === 'object' &&
+ error !== null &&
+ Object.prototype.hasOwnProperty.call(error as Record, 'code')
+ )
+}
diff --git a/src/store/idkit.ts b/src/store/idkit.ts
index 27700ec5..efa793e8 100644
--- a/src/store/idkit.ts
+++ b/src/store/idkit.ts
@@ -1,26 +1,21 @@
import create from 'zustand'
-
-export enum IDKITStage {
- ENTER_PHONE = 'ENTER_PHONE',
- ENTER_CODE = 'ENTER_CODE',
- WORLD_ID = 'WORLD_ID',
- SUCCESS = 'SUCCESS',
- ERROR = 'ERROR',
-}
+import { IDKITStage } from '@/types'
export type IDKitStore = {
open: boolean
+ phoneNumber: string
code: string
actionId: string
stage: IDKITStage
- phoneNumber: string
+ processing: boolean // Whether an async request is being processed and we show a loading state in the UI
retryFlow: () => void
- setCode: (code: string) => void
setOpen: (open: boolean) => void
onOpenChange: (open: boolean) => void
setStage: (stage: IDKITStage) => void
setActionId: (actionId: string) => void
setPhoneNumber: (phoneNumber: string) => void
+ setCode: (code: string) => void
+ setProcessing: (processing: boolean) => void
}
const useIDKitStore = create()(set => ({
@@ -29,16 +24,17 @@ const useIDKitStore = create()(set => ({
actionId: '',
phoneNumber: '',
stage: IDKITStage.ENTER_PHONE,
+ processing: false,
setOpen: open => set({ open }),
+ setPhoneNumber: phoneNumber => set({ phoneNumber }),
setCode: code => set({ code }),
+ setActionId: actionId => set({ actionId }),
setStage: stage => set({ stage }),
- setActionId: (actionId: string) => set({ actionId }),
- setPhoneNumber: (phoneNumber: string) => set({ phoneNumber }),
retryFlow: () => set({ stage: IDKITStage.ENTER_PHONE, phoneNumber: '' }),
+ setProcessing: (processing: boolean) => set({ processing }),
onOpenChange: open => {
if (open) return set({ open })
-
- set({ open, phoneNumber: '', stage: IDKITStage.ENTER_PHONE })
+ set({ open, phoneNumber: '', code: '', processing: false, stage: IDKITStage.ENTER_PHONE })
},
}))
diff --git a/src/types/index.ts b/src/types/index.ts
new file mode 100644
index 00000000..21c972af
--- /dev/null
+++ b/src/types/index.ts
@@ -0,0 +1,13 @@
+export enum IDKITStage {
+ ENTER_PHONE = 'ENTER_PHONE',
+ ENTER_CODE = 'ENTER_CODE',
+ WORLD_ID = 'WORLD_ID',
+ SUCCESS = 'SUCCESS',
+ ERROR = 'ERROR',
+}
+
+export interface IPhoneSignal {
+ success: true
+ nullifier_hash: string
+ signature: string
+}