diff --git a/LENS.md b/LENS.md index aefe56f4..8efe35f8 100644 --- a/LENS.md +++ b/LENS.md @@ -22,9 +22,9 @@ _These instructions assume you're integrating into a React/Next.js app. If you'r 1. Install IDKit JS ```bash - yarn add @worldcoin/idkit + yarn add @worldcoin/idkit@0.3.3 # or - npm install @worldcoin/idkit + npm install @worldcoin/idkit@0.3.3 ``` 2. Load IDKit JS. Note the action ID below, the Action ID is the **same for all Lens-powered apps.** diff --git a/cspell.json b/cspell.json index f124fee9..d4ac9f43 100644 --- a/cspell.json +++ b/cspell.json @@ -21,6 +21,7 @@ "TTFB", "webp", "zustand", - "worldid" + "worldid", + "consts" ] } diff --git a/example-nextjs/pages/index.tsx b/example-nextjs/pages/index.tsx index ad6dd803..7e3c0383 100644 --- a/example-nextjs/pages/index.tsx +++ b/example-nextjs/pages/index.tsx @@ -20,10 +20,11 @@ export default function Home() {
{({ open }) => } diff --git a/example-react/src/App.tsx b/example-react/src/App.tsx index 67e3fc61..dfe54459 100644 --- a/example-react/src/App.tsx +++ b/example-react/src/App.tsx @@ -19,10 +19,11 @@ function App() { style={{ minHeight: "100vh", display: "flex", justifyContent: "center", alignItems: "center" }} > {({ open }) => } diff --git a/idkit/esbuild/production.js b/idkit/esbuild/production.js index 0c574d28..13ff4d7c 100644 --- a/idkit/esbuild/production.js +++ b/idkit/esbuild/production.js @@ -32,7 +32,8 @@ const configs = { esm: { ...baseConfig, - entryPoints: [require.resolve('../src/index.ts')], + entryPoints: [require.resolve('../src/index.ts'), require.resolve('../src/internal.ts')], + outdir: 'build/', define: { ...baseConfig.define, window: 'globalThis', @@ -51,7 +52,6 @@ const configs = { ], sourcemap: true, format: 'esm', - outfile: 'build/index.js', }, iife: { diff --git a/idkit/src/components/CountryCodeSelect.tsx b/idkit/src/components/CountryCodeSelect.tsx deleted file mode 100644 index 4740fd7c..00000000 --- a/idkit/src/components/CountryCodeSelect.tsx +++ /dev/null @@ -1,158 +0,0 @@ -import { classNames } from '@/lib/utils' -import CheckIcon from './Icons/CheckIcon' -import ReactCountryFlag from 'react-country-flag' -import { allCountries } from 'country-telephone-data' -import ChevronDownIcon from './Icons/ChevronDownIcon' -import { Listbox, Transition } from '@headlessui/react' -import { Fragment, useCallback, useEffect, useRef, useState } from 'react' - -type Props = { - value: string - onChange: (value: string) => void -} - -const CountryCodeSelect = ({ value, onChange }: Props) => { - const [countryCode, setCountryCode] = useState('us') - const listRef = useRef(null) - - // FIXME: temporary solution for scroll list - useEffect(() => { - const List = listRef.current - - if (!List) { - return - } - - let touchY = 0 - - const handleWheel = (e: WheelEvent) => { - List.scrollTop += e.deltaY - } - - const handleTouchStart = (e: TouchEvent) => { - touchY = List.scrollTop + e.touches[0].pageY - } - - const handleTouchMove = (e: TouchEvent) => { - List.scrollTop = touchY - e.touches[0].pageY - } - - List.addEventListener('wheel', handleWheel) - List.addEventListener('touchstart', handleTouchStart) - List.addEventListener('touchmove', handleTouchMove) - - return () => { - List.removeEventListener('wheel', handleWheel) - List.removeEventListener('touchstart', handleTouchStart) - List.removeEventListener('touchmove', handleTouchMove) - } - }, []) - - const handleSetListHeight = useCallback(() => { - const List = listRef.current - if (!List) return - - const bounds = List.getBoundingClientRect() - - List.style.maxHeight = `${window.innerHeight - bounds.top - 12}px` - }, []) - - useEffect(() => { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - onChange(allCountries.find(country => country.iso2 === countryCode)!.dialCode) - }, [countryCode, onChange]) - - return ( - - {({ open }) => ( - <> - - Country Code - - - -

+{value}

-
- - - -
- {allCountries.map(country => ( - - classNames( - 'dark:text-white', - selected - ? 'bg-0d151d dark:bg-white text-white dark:text-0d151d font-medium' - : active - ? 'bg-0d151d/5 dark:bg-ece8fb/10' - : 'text-gray-900 dark:text-white', - 'relative flex items-center justify-between px-2 py-2 w-full rounded-xl text-sm focus:bg-gray-100 dark:focus:bg-ece8fb/25', - 'focus:outline-none select-none' - ) - } - value={country.iso2} - > - {({ selected }) => ( - <> -
- -

{country.name}

- {selected ? ( - - - ) : null} -
- - +{country.dialCode} - - - )} -
- ))} -
-
-
- - )} -
- ) -} - -export default CountryCodeSelect diff --git a/idkit/src/components/IDKitWidget/BaseWidget.tsx b/idkit/src/components/IDKitWidget/BaseWidget.tsx index 5f0a7abc..1948aba6 100644 --- a/idkit/src/components/IDKitWidget/BaseWidget.tsx +++ b/idkit/src/components/IDKitWidget/BaseWidget.tsx @@ -19,11 +19,8 @@ import { classNames, getCopy } from '@/lib/utils' import type { WidgetProps } from '@/types/config' import { Fragment, useEffect, useMemo } from 'react' import WorldIDWordmark from '../Icons/WorldIDWordmark' -import EnterPhoneState from './States/EnterPhoneState' -import VerifyCodeState from './States/VerifyCodeState' import { AnimatePresence, motion } from 'framer-motion' import ArrowLongLeftIcon from '../Icons/ArrowLongLeftIcon' -import SelectMethodState from './States/SelectMethodState' import HostAppVerificationState from './States/HostAppVerificationState' const getParams = ({ @@ -46,14 +43,14 @@ const getParams = ({ isOpen: open, onOpenChange, canGoBack: computed.canGoBack(stage), - defaultStage: computed.getDefaultStage(), }) const IDKitWidget: FC = ({ children, - actionId, + app_id, + action, + action_description, theme, - methods, signal, walletConnectProjectId, handleVerify, @@ -68,7 +65,6 @@ const IDKitWidget: FC = ({ stage, setStage, canGoBack, - defaultStage, setOptions, copy: _copy, theme: _theme, @@ -77,21 +73,38 @@ const IDKitWidget: FC = ({ useEffect(() => { setOptions( - { actionId, signal, walletConnectProjectId, methods, onSuccess, handleVerify, autoClose, copy, theme }, + { + app_id, + action, + action_description, + signal, + walletConnectProjectId, + onSuccess, + handleVerify, + autoClose, + copy, + theme, + }, ConfigSource.PROPS ) - }, [actionId, signal, walletConnectProjectId, methods, onSuccess, theme, handleVerify, autoClose, copy, setOptions]) + }, [ + copy, + app_id, + action, + theme, + signal, + autoClose, + onSuccess, + setOptions, + handleVerify, + action_description, + walletConnectProjectId, + ]) const StageContent = useMemo(() => { switch (stage) { - case IDKITStage.SELECT_METHOD: - return SelectMethodState - case IDKITStage.ENTER_PHONE: - return EnterPhoneState case IDKITStage.WORLD_ID: return WorldIDState - case IDKITStage.ENTER_CODE: - return VerifyCodeState case IDKITStage.SUCCESS: return SuccessState case IDKITStage.ERROR: @@ -151,7 +164,7 @@ const IDKitWidget: FC = ({
-
-
- )} -
- ) -} - -export default EnterPhoneState diff --git a/idkit/src/components/IDKitWidget/States/SelectMethodState.tsx b/idkit/src/components/IDKitWidget/States/SelectMethodState.tsx deleted file mode 100644 index 72d44af8..00000000 --- a/idkit/src/components/IDKitWidget/States/SelectMethodState.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import type { FC } from 'react' -import { IDKITStage } from '@/types' -import { motion } from 'framer-motion' -import useIDKitStore from '@/store/idkit' -import type { IDKitStore } from '@/store/idkit' -import { classNames, getCopy } from '@/lib/utils' -import WorldIDIcon from '@/components/WorldIDIcon' -import AboutWorldID from '@/components/AboutWorldID' -import type { VerificationMethods } from '@/types/config' -import DevicePhoneMobileIcon from '@/components/Icons/DevicePhoneMobileIcon' - -const getParams = ({ setStage, methods, copy }: IDKitStore) => ({ - copy, - methods, - setStage, -}) - -const SelectMethodState = () => { - const { copy, setStage, methods } = useIDKitStore(getParams) - - return ( -
-
-

- {getCopy(copy, 'heading')} -

-

{getCopy(copy, 'subheading')}

-
-
- {methods.map((method, i) => ( - - ))} -
- {methods.includes('orb') && ( -
-

- Don't have your World ID yet?{' '} - - Download Now - -

-
- )} - -
- ) -} - -const MethodButton: FC<{ primary: boolean; setStage: IDKitStore['setStage']; method: VerificationMethods }> = ({ - method, - primary, - setStage, -}) => ( - setStage(method == 'orb' ? IDKITStage.WORLD_ID : IDKITStage.ENTER_PHONE)} - className={classNames( - 'flex w-full space-x-2 items-center px-4 py-4 border border-transparent font-medium rounded-2xl shadow-sm', - primary - ? 'bg-0d151d dark:bg-white text-white dark:text-0d151d' - : 'bg-d3dfea/30 dark:bg-29343f text-0d151d dark:text-white' - )} - > - {method == 'orb' ? ( - - ) : ( - - )} - - {method == 'orb' ? 'Verify with World ID' : 'Verify with Phone Number'} - - -) - -export default SelectMethodState diff --git a/idkit/src/components/IDKitWidget/States/VerifyCodeState.tsx b/idkit/src/components/IDKitWidget/States/VerifyCodeState.tsx deleted file mode 100644 index bdc4cb88..00000000 --- a/idkit/src/components/IDKitWidget/States/VerifyCodeState.tsx +++ /dev/null @@ -1,133 +0,0 @@ -import { motion } from 'framer-motion' -import { useMemo, useRef } from 'react' -import { classNames } from '@/lib/utils' -import useIDKitStore from '@/store/idkit' -import type { IDKitStore } from '@/store/idkit' -import { getTelemetryId } from '@/lib/telemetry' -import WorldIDIcon from '@/components/WorldIDIcon' -import ResendButton from '@/components/ResendButton' -import SMSCodeInput from '@/components/SMSCodeInput' -import { ErrorCodes, IDKITStage, SignalType } from '@/types' -import { isVerifyCodeError, verifyCode } from '@/services/phone' - -const getParams = ({ - processing, - phoneNumber, - code, - stringifiedActionId, - setStage, - setProcessing, - setCode, - copy, - handleVerify, - setErrorState, - errorState, -}: IDKitStore) => ({ - processing, - phoneNumber, - code, - stringifiedActionId, - errorState, - setCode, - setErrorState, - copy, - onSubmit: async () => { - try { - setErrorState(null) - setProcessing(true) - const { nullifier_hash, ...proof_payload } = await verifyCode( - phoneNumber, - code, - stringifiedActionId, - getTelemetryId() - ) - handleVerify({ signal_type: SignalType.Phone, nullifier_hash, proof_payload }) - } catch (error) { - setProcessing(false) - setCode('') - if (isVerifyCodeError(error)) { - setErrorState({ code: ErrorCodes.INVALID_CODE }) - console.error(error) - } else { - setStage(IDKITStage.ERROR) - } - } - }, - useWorldID: () => setStage(IDKITStage.WORLD_ID), -}) - -const VerifyCodeState = () => { - const submitRef = useRef(null) - const { phoneNumber, processing, code, onSubmit, useWorldID, errorState } = useIDKitStore(getParams) - - const animation = useMemo(() => { - if (!processing && errorState) { - return { x: [0, -16, 16, -8, 8, 0] } - } - }, [processing, errorState]) - - return ( -
-
-

- Enter your 6-digit code to complete the verification. -

-

- Code has been sent to {phoneNumber} -

-
-
- - - -

- {errorState ? ( - That code is invalid. Please try again. - ) : ( - 'Did not receive a code?' - )}{' '} - -

-
-
-
- -

I have World ID

-
- - -
-
- void onSubmit()} - disabled={!code || processing} - ref={submitRef} - className={classNames( - 'flex w-full items-center justify-center px-8 py-4 border border-transparent font-medium rounded-xl shadow-sm', - 'bg-0d151d dark:bg-white text-white dark:text-0d151d', - 'focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-0d151d', - 'disabled:cursor-not-allowed disabled:bg-d3dfea dark:disabled:bg-29343f' - )} - > - - Continue - - -
-
- ) -} - -export default VerifyCodeState diff --git a/idkit/src/components/IDKitWidget/States/WorldIDState.tsx b/idkit/src/components/IDKitWidget/States/WorldIDState.tsx index 5fbc3cd6..ce861f96 100644 --- a/idkit/src/components/IDKitWidget/States/WorldIDState.tsx +++ b/idkit/src/components/IDKitWidget/States/WorldIDState.tsx @@ -1,3 +1,4 @@ +import { IDKITStage } from '@/types' import { getCopy } from '@/lib/utils' import useMedia from '@/hooks/useMedia' import QRState from './WorldID/QRState' @@ -5,30 +6,34 @@ import useIDKitStore from '@/store/idkit' import { useEffect, useState } from 'react' import { VerificationState } from '@/types/orb' import type { IDKitStore } from '@/store/idkit' -import { IDKITStage, SignalType } from '@/types' import useOrbSignal from '@/services/walletconnect' import AboutWorldID from '@/components/AboutWorldID' import LoadingIcon from '@/components/Icons/LoadingIcon' -import DevicePhoneMobileIcon from '@/components/Icons/DevicePhoneMobileIcon' const getOptions = (store: IDKitStore) => ({ signal: store.signal, copy: store.copy, - actionId: store.actionId, + app_id: store.app_id, + action: store.action, + action_description: store.action_description, walletConnectProjectId: store.walletConnectProjectId, handleVerify: store.handleVerify, - showAbout: store.methods.length == 1, - hasPhone: store.methods.includes('phone'), - usePhone: () => store.setStage(IDKITStage.ENTER_PHONE), setStage: store.setStage, }) const WorldIDState = () => { const media = useMedia() const [showQR, setShowQR] = useState(false) - const { actionId, copy, signal, walletConnectProjectId, handleVerify, showAbout, hasPhone, usePhone, setStage } = + const { copy, app_id, action, signal, setStage, handleVerify, action_description, walletConnectProjectId } = useIDKitStore(getOptions) - const { result, qrData, verificationState, reset } = useOrbSignal(actionId, signal, walletConnectProjectId) + + const { result, qrData, verificationState, reset } = useOrbSignal( + app_id, + action, + signal, + action_description, + walletConnectProjectId + ) useEffect(() => reset, [reset]) @@ -37,15 +42,7 @@ const WorldIDState = () => { setStage(IDKITStage.ERROR) } - if (!result) return - handleVerify({ - signal_type: SignalType.Orb, - nullifier_hash: result.nullifier_hash, - proof_payload: { - proof: result.proof, - merkle_root: result.merkle_root, - }, - }) + if (result) handleVerify(result) }, [result, reset, handleVerify, verificationState, setStage]) return ( @@ -70,22 +67,7 @@ const WorldIDState = () => { ) : ( )} - {showAbout && (media == 'desktop' || !showQR) && } - {hasPhone && verificationState == VerificationState.AwaitingConnection && ( -
-
-
-

or

-
-
-
- -
-
- )} + {(media == 'desktop' || !showQR) && }
) } diff --git a/idkit/src/components/PhoneInput.tsx b/idkit/src/components/PhoneInput.tsx deleted file mode 100644 index d36e4994..00000000 --- a/idkit/src/components/PhoneInput.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { phone } from 'phone' -import useIDKitStore from '@/store/idkit' -import type { IDKitStore } from '@/store/idkit' -import CountryCodeSelect from './CountryCodeSelect' -import { Fragment, memo, useEffect, useState } from 'react' - -const getParams = ({ setPhoneNumber }: IDKitStore) => ({ setFullPhone: setPhoneNumber }) - -const PhoneInput = ({ disabled, onSubmit }: { disabled?: boolean; onSubmit?: () => Promise | void }) => { - const { setFullPhone } = useIDKitStore(getParams) - const [countryCode, setCountryCode] = useState('1') - const [phoneNumber, setPhoneNumber] = useState('') - - useEffect(() => { - const validatedPhone = phone(`+${countryCode} ${phoneNumber}`) - if (!validatedPhone.isValid) { - setFullPhone('') - return - } - - setFullPhone(validatedPhone.phoneNumber) - }, [countryCode, phoneNumber, setFullPhone]) - - return ( - - -
-
- - -
- setPhoneNumber(e.target.value)} - className="placeholder:text-9eafc0 block w-full rounded-md border-transparent bg-transparent pl-6 focus:border-transparent focus:ring-transparent dark:text-white sm:text-sm" - disabled={disabled} - onKeyDown={e => e.key === 'Enter' && void onSubmit?.()} - /> -
-
- ) -} - -export default memo(PhoneInput) diff --git a/idkit/src/components/ResendButton.tsx b/idkit/src/components/ResendButton.tsx deleted file mode 100644 index 3dd78efe..00000000 --- a/idkit/src/components/ResendButton.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import { memo, useState } from 'react' -import Countdown from 'react-countdown' -import useIDKitStore from '@/store/idkit' -import { ErrorCodes, IDKITStage } from '@/types' -import { PhoneVerificationChannel } from '@/types' -import { isRequestCodeError, requestCode } from '@/services/phone' -import { getTelemetryId, telemetryRetryCode } from '@/lib/telemetry' - -const ONE_MINUTE = 59 * 1000 - -const renderer = ({ minutes, seconds, completed }: { minutes: number; seconds: number; completed: boolean }) => { - if (completed) return - - return ( - - in {minutes}:{seconds < 10 ? `0${seconds}` : seconds} - - ) -} - -const ResendButton = () => { - const { phoneNumber, stringifiedActionId, setProcessing, setStage, setErrorState } = useIDKitStore() - const [nextTime, setNextTime] = useState(new Date().getTime() + ONE_MINUTE) - - const handleRetry = async (channel: PhoneVerificationChannel) => { - setNextTime(new Date().getTime() + ONE_MINUTE) - try { - setProcessing(true) - await requestCode(phoneNumber, stringifiedActionId, channel, getTelemetryId()) - telemetryRetryCode(channel) - setProcessing(false) - } catch (error) { - setProcessing(false) - setNextTime(undefined) - if (isRequestCodeError(error) && error.code !== 'server_error') { - setErrorState({ code: ErrorCodes.GENERIC_ERROR }) - console.error(error) - } else { - setStage(IDKITStage.ERROR) - } - } - } - - return ( - <> - {' '} - or{' '} - - - ) -} - -export default memo(ResendButton) diff --git a/idkit/src/components/SMSCodeInput.tsx b/idkit/src/components/SMSCodeInput.tsx deleted file mode 100644 index e50b2f72..00000000 --- a/idkit/src/components/SMSCodeInput.tsx +++ /dev/null @@ -1,176 +0,0 @@ -import { classNames } from '@/lib/utils' -import useIDKitStore from '@/store/idkit' -import type { IDKitStore } from '@/store/idkit' -import type { ChangeEvent, ClipboardEvent, KeyboardEvent, RefObject } from 'react' -import { createRef, memo, useCallback, useEffect, useMemo, useState } from 'react' - -type Array6 = [T, T, T, T, T, T] - -const fillValues = (value: string): Array6 => { - return new Array(6).fill('').map((_, index) => value[index] || '') as Array6 -} - -const getParams = ({ code, setCode }: IDKitStore) => ({ code, setCode }) - -const SMSCodeInput = ({ submitRef, disabled }: { submitRef: RefObject; disabled?: boolean }) => { - const { code, setCode } = useIDKitStore(getParams) - - const inputsRefs = useMemo(() => new Array(6).fill(null).map(() => createRef()), []) - const [values, setValues] = useState>(fillValues('')) - const [focusedIndex, setFocusedIndex] = useState(-1) - - useEffect(() => { - if (!code) { - setValues(fillValues('')) - } - }, [code]) - - const selectInputContent = useCallback( - (index: number) => { - requestAnimationFrame(() => { - inputsRefs[index].current?.select() - }) - }, - [inputsRefs] - ) - - const setValue = useCallback( - (value: string, index: number) => { - const nextValues = [...values] - nextValues[index] = value - - setValues(nextValues as Array6) - - const stringifiedValues = nextValues.join('') - - setCode('') - if (stringifiedValues.length !== 6) return - - setCode(stringifiedValues) - }, - [setCode, values] - ) - - const focusInput = useCallback( - (index: number) => { - requestAnimationFrame(() => { - inputsRefs[index]?.current?.focus() - }) - }, - [inputsRefs] - ) - - const blurInput = useCallback( - (index: number) => { - requestAnimationFrame(() => { - inputsRefs[index].current?.blur() - }) - }, - [inputsRefs] - ) - - const onInputFocus = (index: number) => { - if (!inputsRefs[index]?.current) return - - setFocusedIndex(index) - selectInputContent(index) - } - - const onInputChange = useCallback( - (event: ChangeEvent, index: number) => { - const value = event.target.value.replace(values[index], '') - - if (!/^\d/.test(value)) return selectInputContent(index) - - if (value.length > 1) { - setValues(fillValues(event.target.value)) - - if (event.target.value.length !== 6) return - - setCode(event.target.value) - return blurInput(index) - } - - setValue(value, index) - - if (index === 5) { - requestAnimationFrame(() => { - submitRef.current?.focus() - }) - } else focusInput(index + 1) - }, - [blurInput, submitRef, focusInput, selectInputContent, setCode, setValue, values] - ) - - const onInputKeyDown = useCallback( - (event: KeyboardEvent, index: number) => { - if (event.key === 'Backspace' || event.key === 'Delete') { - event.preventDefault() - - setValue('', focusedIndex) - return focusInput(index - 1) - } - - if (event.key === values[index]) { - if (index === 5) return blurInput(index) - - focusInput(index + 1) - } - }, - [focusInput, blurInput, focusedIndex, setValue, values] - ) - - const onInputPaste = useCallback( - (event: ClipboardEvent, index: number) => { - event.preventDefault() - - const pastedValue = event.clipboardData.getData('text') - const nextValues = pastedValue.slice(0, 6) - - if (!/^\d/.test(nextValues)) return - - setValues(fillValues(nextValues)) - if (nextValues.length !== 6) return focusInput(nextValues.length) - - setCode(nextValues) - blurInput(index) - }, - [blurInput, focusInput, setCode] - ) - - useEffect(() => { - focusInput(0) - }, [focusInput, inputsRefs]) - - return ( -
- Enter your SMS code - {inputsRefs.map((ref, i) => ( - onInputFocus(i)} - onPaste={event => onInputPaste(event, i)} - onChange={event => onInputChange(event, i)} - onKeyDown={event => onInputKeyDown(event, i)} - className={classNames( - 'w-10 xs:w-12 aspect-[6/7] border-0 outline-0 rounded-xl text-center caret-transparent text-29343f dark:text-white', - 'bg-gray-100 dark:bg-29343f focus:bg-transparent', - 'ring ring-transparent focus:ring-5b52f3' - )} - disabled={disabled} - /> - ))} -
- ) -} - -export default memo(SMSCodeInput) diff --git a/idkit/src/index.html b/idkit/src/index.html index 99e450fe..b0263d5c 100644 --- a/idkit/src/index.html +++ b/idkit/src/index.html @@ -41,7 +41,9 @@

idkit-js