Skip to content

Instantly share code, notes, and snippets.

@garand
Last active July 2, 2020 17:21
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save garand/9403725af3ef3ff3f7047df28581b1ff to your computer and use it in GitHub Desktop.
Save garand/9403725af3ef3ff3f7047df28581b1ff to your computer and use it in GitHub Desktop.
useSqPaymentForm React Hook
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,
},
}
}
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