Last active
July 2, 2020 17:21
-
-
Save garand/9403725af3ef3ff3f7047df28581b1ff to your computer and use it in GitHub Desktop.
useSqPaymentForm React Hook
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { useState, useEffect, useRef, useCallback } from 'react' | |
import { | |
UseSqPaymentForm, | |
states, | |
SqResponseError, | |
SqCardData, | |
SqVerificationResult, | |
SqVerificationError, | |
SqInputEventData, | |
SqContact, | |
SqShippingOption, | |
UseSqPaymentFormResponse, | |
} from './useSqPaymentForm.types' | |
export const useSqPaymentForm: UseSqPaymentForm = ({ | |
config, | |
options, | |
callbacks, | |
}) => { | |
const defaultConfig = {} | |
config = { ...defaultConfig, ...config } | |
const defaultOptions = { | |
verifyBuyer: true, // Run verify buyer when a nonce response is recieved | |
} | |
options = { ...defaultOptions, ...options } | |
const defaultResponse: UseSqPaymentFormResponse = { | |
errors: { | |
nonce: null, | |
verification: null, | |
}, | |
nonce: null, | |
cardData: null, | |
billingContact: null, | |
shippingContact: null, | |
shippingOption: null, | |
verificationResult: null, | |
} | |
// Setup initial state | |
const [instance, setInstance] = useState(null) | |
const instanceRef = useRef(null) | |
instanceRef.current = instance | |
const [state, setState] = useState(states.INITIALIZING) | |
const [response, setResponse] = useState(defaultResponse) | |
const [formLoaded, setFormLoaded] = useState(false) | |
const [verificationDetails, setVerificationDetails] = useState() | |
const verificationDetailsRef = useRef(null) | |
verificationDetailsRef.current = verificationDetails | |
// | |
// Load SqPaymentForm JS library | |
// ------------------------------------------ | |
// | |
useEffect(() => { | |
// Only run if the state is 'INITIALIZING' | |
// This will load the SqPaymentForm JS library on the initial render | |
if (state === states.INITIALIZING) { | |
setState(states.LOADING_API) | |
const sqPaymentScript = document.createElement('script') | |
// Load the sanxbox script if the | |
// applicationId starts with 'sandbox' | |
// otherwise, load the production script | |
sqPaymentScript.src = `https://js.squareup${ | |
config?.applicationId.startsWith('sandbox') ? 'sandbox' : '' | |
}.com/v2/paymentform` | |
sqPaymentScript.type = 'text/javascript' | |
sqPaymentScript.async = false | |
sqPaymentScript.onload = (): void => { | |
setState(states.API_LOADED) | |
} | |
document.getElementsByTagName('head')[0].appendChild(sqPaymentScript) | |
} | |
}, [config, state]) | |
// | |
// Initialize SqPaymentForm | |
// ------------------------------------------ | |
// | |
useEffect(() => { | |
// Create the Payment Form instance if the Square Payment Form API | |
// is loaded and no payment form instance already exists | |
if (state === states.API_LOADED && !instance) { | |
try { | |
setInstance( | |
// @ts-expect-error | |
// eslint-disable-next-line no-undef | |
new SqPaymentForm({ | |
...config, | |
callbacks: { | |
...callbacks, | |
paymentFormLoaded: (): void => { | |
setFormLoaded(true) | |
setState(states.IDLE) | |
// Call function passed to hook if it exists | |
if (typeof callbacks?.paymentFormLoaded === 'function') | |
callbacks?.paymentFormLoaded() | |
}, | |
inputEventReceived: (inputEvent: SqInputEventData): void => { | |
// Call function passed to hook if it exists | |
if (typeof callbacks?.inputEventReceived === 'function') | |
callbacks?.inputEventReceived(instanceRef.current, inputEvent) | |
}, | |
cardNonceResponseReceived: ( | |
errors: SqResponseError[], | |
nonce: string, | |
cardData: SqCardData, | |
billingContact: SqContact, | |
shippingContact: SqContact, | |
shippingOption: SqShippingOption, | |
): void => { | |
// Call function passed to hook if it exists | |
if (typeof callbacks?.cardNonceResponseReceived === 'function') | |
callbacks?.cardNonceResponseReceived( | |
instanceRef.current, | |
errors, | |
nonce, | |
cardData, | |
billingContact, | |
shippingContact, | |
shippingOption, | |
) | |
setResponse((response) => { | |
return { | |
...response, | |
errors: { | |
...response.errors, | |
nonce: errors, | |
}, | |
nonce: nonce, | |
cardData: cardData, | |
billingContact: billingContact, | |
shippingContact: shippingContact, | |
shippingOption: shippingOption, | |
} | |
}) | |
if (errors) { | |
setState(states.ERROR) | |
return | |
} else { | |
setState(states.NONCE_RECEIVED) | |
} | |
if (options.verifyBuyer) { | |
// Exit if the verifyBuyer option is true, | |
// but no verification details are provided | |
if (!verificationDetailsRef.current) { | |
console.error( | |
`verifiyBuyer set to true, but verificationDetails was ${typeof verificationDetailsRef.current}`, | |
) | |
return | |
} | |
setState(states.VERIFYING_BUYER) | |
instanceRef.current.verifyBuyer( | |
nonce, | |
verificationDetailsRef.current, | |
( | |
errors: SqVerificationError[], | |
verificationResult: SqVerificationResult, | |
) => { | |
// Call function passed to hook if it exists | |
if ( | |
typeof callbacks?.verifyBuyerResponseReceived === | |
'function' | |
) | |
callbacks?.verifyBuyerResponseReceived( | |
errors, | |
verificationResult, | |
) | |
if (errors) { | |
setState(states.ERROR) | |
} | |
setResponse((response) => { | |
const finalResponse = { | |
...response, | |
errors: { ...response.errors, verification: errors }, | |
verificationResult, | |
} | |
// Call function passed to hook if it exists | |
if ( | |
typeof callbacks?.paymentFormResponseComplete === | |
'function' | |
) | |
callbacks?.paymentFormResponseComplete( | |
finalResponse, | |
verificationDetailsRef?.current?.billingContact, | |
) | |
return finalResponse | |
}) | |
}, | |
) | |
} | |
setState(states.SUCCESS) | |
}, | |
}, | |
}), | |
) | |
} catch (e) { | |
switch (e.name) { | |
case 'HttpsRequiredError': | |
setState(states.HTTPS_REQUIRED) | |
break | |
default: | |
setState(states.SYSTEM_ERROR) | |
break | |
} | |
console.log(e) | |
} | |
} | |
}, [state, config, callbacks, instance, response, options.verifyBuyer]) | |
// | |
// Build SqPaymentForm | |
// ------------------------------------------ | |
// | |
useEffect(() => { | |
if (instance && !formLoaded) { | |
setState(states.BUILDING_FORM) | |
instance?.build() | |
} | |
}, [instance, formLoaded]) | |
const getNonce = useCallback( | |
(verificationDetails) => { | |
setState(states.REQUESTING_NONCE) | |
if (verificationDetails) setVerificationDetails(verificationDetails) | |
instance.requestCardNonce() | |
}, | |
[instance], | |
) | |
return { | |
instance, | |
state, | |
response, | |
actions: { | |
getNonce, | |
}, | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export interface UseSqPaymentForm { | |
(props: UseSqPaymentFormProps): UseSqPaymentFormReturn; | |
} | |
export type UseSqPaymentFormProps = { | |
config: UseSqPaymentFormConfig; | |
options?: UseSqPaymentFormOptions; | |
callbacks?: SqPaymentFormCallbacks; | |
} | |
export type UseSqPaymentFormReturn = { | |
instance: SqPaymentFormInstance; | |
state: states; | |
response: UseSqPaymentFormResponse; | |
actions: { | |
getNonce: UseSqPaymentFormGetNonce; | |
}; | |
} | |
export type UseSqPaymentFormGetNonce = ( | |
verificationDetails: SqVerificationDetails, | |
) => void | |
export type UseSqPaymentFormConfig = { | |
applePay?: InputTarget; | |
applicationId: string; | |
autoBuild?: boolean; | |
callbacks?: SqPaymentFormCallbacks; | |
card?: InputTarget; | |
cardNumber?: InputTarget; | |
cvv?: InputTarget; | |
expirationDate?: InputTarget; | |
giftCard?: InputTarget; | |
googlePay?: InputTarget; | |
locationId?: string; | |
masterpass?: InputTarget; | |
postalCode?: InputTarget; | |
inputClass?: string; | |
inputStyles?: InputStyle[]; | |
} | |
export type UseSqPaymentFormOptions = { | |
verifyBuyer: boolean; | |
} | |
export type UseSqPaymentFormResponse = { | |
errors: { | |
nonce: SqResponseError[]; | |
verification: SqVerificationError[]; | |
}; | |
nonce: string; | |
cardData: SqCardData; | |
billingContact: SqContact; | |
shippingContact: SqContact; | |
shippingOption: SqShippingOption; | |
verificationResult: SqVerificationResult; | |
} | |
export enum states { | |
INITIALIZING = 'INITIALIZING', | |
LOADING_API = 'LOADING_API', | |
API_LOADED = 'API_LOADED', | |
BUILDING_FORM = 'BUILDING_FORM', | |
IDLE = 'IDLE', | |
REQUESTING_NONCE = 'REQUESTING_NONCE', | |
NONCE_RECEIVED = 'NONCE_RECEIVED', | |
VERIFYING_BUYER = 'VERIFYING_BUYER', | |
SUCCESS = 'SUCCESS', | |
ERROR = 'ERROR', | |
HTTPS_REQUIRED = 'HTTPS_REQUIRED', | |
SYSTEM_ERROR = 'SYSTEM_ERROR', | |
} | |
// SqPaymentForm Data Types | |
// https://developer.squareup.com/docs/api/paymentform#sqpaymentrequestdatatypes | |
export type SqPaymentFormInstance = { | |
build: () => void; | |
destroy: () => void; | |
done: (paymentDetailsUpdate: object) => boolean; | |
isSupportedBrowser: () => boolean; | |
masterpassImageUrl: (assetUrl: string) => void; | |
recalculateSize: () => void; | |
setPostalCode: (postalCod: string) => void; | |
focus: (field: 'cardNumber' | 'cvv' | 'expirationDate' | 'postalCode') => void; | |
requestCardNonce: () => void; | |
verifyBuyer: () => void; | |
} | |
export type SqPaymentFormCallbacks = { | |
cardNonceResponseReceived?: ( | |
instance: SqPaymentFormInstance, | |
errors: SqResponseError[], | |
nonce: string, | |
cardData: SqCardData, | |
billingContact: SqContact, | |
shippingContact: SqContact, | |
shippingOption: SqShippingOption, | |
) => void; | |
createPaymentRequest?: () => void; | |
inputEventReceived?: ( | |
instance: SqPaymentFormInstance, | |
inputEvent: SqInputEventData, | |
) => void; | |
methodsSupported?: () => void; | |
paymentFormLoaded?: () => void; | |
unsupportedBrowserDetected?: () => void; | |
shippingContactChanged?: () => void; | |
shippingOptionChanged?: () => void; | |
verifyBuyerResponseReceived?: ( | |
errors: SqVerificationError[], | |
verificationResult: SqVerificationResult, | |
) => void; | |
paymentFormResponseComplete?: ( | |
response: UseSqPaymentFormResponse, | |
billingContact: SqContact, | |
) => void; | |
} | |
export type SqInputEventData = { | |
cardBrand: string; | |
currentState: SqInputEventDataState; | |
elementId: string; | |
eventType: string; | |
field: string; | |
postalCodeValue: string; | |
previousState: SqInputEventDataState; | |
} | |
export type SqInputEventDataState = { | |
hasErrorClass: boolean; | |
hasFocusClass: boolean; | |
isCompletelyValid: boolean; | |
isEmpty: boolean; | |
isPotentiallyValid: boolean; | |
} | |
export type SqResponseError = { | |
type: string; | |
message: string; | |
field: string; | |
} | |
export type SqContact = { | |
familyName: string; | |
givenName: string; | |
email: string; | |
country: string; | |
countryName: string; | |
region: string; | |
city: string; | |
addressLines: string[]; | |
postalCode: string; | |
phone: string; | |
} | |
export type SqShippingOption = { | |
id: string; | |
label: string; | |
amount: string; | |
} | |
export type SqVerificationDetails = { | |
billingContact: SqContact; | |
amount: string; | |
currencyCode: string; | |
intent: 'CHARGE' | 'STORE'; | |
} | |
export type SqVerificationError = { | |
type: string; | |
message: string; | |
} | |
export type SqVerificationResult = { | |
token: string; | |
userChallenged: boolean; | |
} | |
export type SqCardData = { | |
card_brand: CardBrands; | |
last_4: string; | |
exp_month: string; | |
exp_year: string; | |
billing_postal_code: string; | |
digital_wallet_type: DigitalWalletTypes; | |
} | |
export type InputTarget = { | |
elementId: string; | |
placeholder?: string; | |
inputStyle?: InputStyle; | |
} | |
export type InputStyle = { | |
mediaMinWidth?: string; | |
mediaMaxWidth?: string; | |
backgroundColor?: string; | |
borderRadius?: string; | |
boxShadow?: string; | |
color?: string; | |
details?: InputStyleDetails; | |
error?: InputStyleError; | |
fontFamily?: SqFontFamily; | |
fontSize?: string; | |
fontWeight?: string; | |
letterSpacing?: string; | |
lineHeight?: string; | |
padding?: string; | |
placeholderColor?: string; | |
placeholderFontWeight?: string; | |
} | |
export type InputStyleDetails = { | |
hidden?: boolean; | |
color?: string; | |
fontFmaily?: SqFontFamily; | |
fontSize?: string; | |
fontWeight?: string; | |
error?: InputStyleError; | |
} | |
export type InputStyleError = { | |
backgroundColor?: string; | |
boxShadow?: string; | |
cardIconColor?: string; | |
color?: string; | |
fontFamily?: SqFontFamily; | |
fontSize?: string; | |
fontWeight?: string; | |
outlineColor?: string; | |
} | |
export type CardBrands = | |
| 'americanExpress' | |
| 'discover' | |
| 'discoverDiners' | |
| 'JCB' | |
| 'masterCard' | |
| 'unionPay' | |
| 'unknown' | |
| 'visa' | |
export type DigitalWalletTypes = | |
| 'APPLE_PAY' | |
| 'GOOGLE_PAY' | |
| 'MASTERPASS' | |
| 'NONE' | |
export type SqFontFamily = | |
| 'andale mono' | |
| 'arial' | |
| 'arial black' | |
| 'arial narrow' | |
| 'arial rounded mt bold' | |
| 'avant garde' | |
| 'baskerville' | |
| 'big caslon' | |
| 'bodoni mt' | |
| 'book antiqua' | |
| 'brush script mt' | |
| 'calibri' | |
| 'calisto mt' | |
| 'cambria' | |
| 'candara' | |
| 'century gothic' | |
| 'charcoal' | |
| 'comic sans ms' | |
| 'consolas' | |
| 'copperplate' | |
| 'copperplate gothic light' | |
| 'courier' | |
| 'courier new' | |
| 'cursive' | |
| 'didot' | |
| 'fantasy' | |
| 'franklin gothic medium' | |
| 'futura' | |
| 'garamond' | |
| 'geneva' | |
| 'georgia' | |
| 'gill sans' | |
| 'goudy old style' | |
| 'helvetica' | |
| 'helvetica neue' | |
| 'hoefler text' | |
| 'impact' | |
| 'lucida bright' | |
| 'lucida console' | |
| 'lucida grande' | |
| 'lucida sans unicode' | |
| 'lucida sans typewriter' | |
| 'monaco' | |
| 'monospace' | |
| 'optima' | |
| 'palatino' | |
| 'palatino linotype' | |
| 'papyrus' | |
| 'perpetua' | |
| 'rockwell' | |
| 'rockwell extra bold' | |
| 'sans-serif' | |
| 'segoe ui' | |
| 'serif' | |
| 'tahoma' | |
| 'times' | |
| 'times new roman' | |
| 'trebuchet ms' | |
| 'verdana' |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment