-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[UXP-3241] Card form component wrapper (#179)
* Creates DuffelCardForm component * version bump --------- Co-authored-by: Igor de Paula <[email protected]>
- Loading branch information
Showing
13 changed files
with
297 additions
and
6 deletions.
There are no files selected for viewing
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
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
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
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
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
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
import * as React from "react"; | ||
import { getTokenFromClientKey } from "./lib/getTokenFromClientKey"; | ||
import { DuffelCardFormProps } from "./lib/types"; | ||
import { getIFrameEventListener } from "./lib/getIFrameEventListener"; | ||
|
||
const LOCAL_TOKEN_PROXY_IFRAME_BASE_URL = "https://localhost:8000/iframe.html"; | ||
|
||
export const DuffelCardForm: React.FC<DuffelCardFormProps> = ({ | ||
clientKey, | ||
styles, | ||
shouldUseLocalTokenProxy, | ||
|
||
actions, | ||
|
||
onValidateSuccess, | ||
onValidateFailure, | ||
|
||
onCreateCardForTemporaryUseSuccess, | ||
onCreateCardForTemporaryUseFailure, | ||
}) => { | ||
const baseUrlString = shouldUseLocalTokenProxy | ||
? LOCAL_TOKEN_PROXY_IFRAME_BASE_URL | ||
: process.env.TOKEN_PROXY_IFRAME_BASE_URL; | ||
|
||
if (typeof baseUrlString !== "string") { | ||
throw new Error("TOKEN_PROXY_IFRAME_BASE_URL is not defined"); | ||
} | ||
|
||
const baseUrl = new URL(baseUrlString); | ||
|
||
const [iFrameHeight, setIFrameHeight] = React.useState("0px"); | ||
|
||
const iFrameReference = React.useRef<HTMLIFrameElement>(null); | ||
|
||
const params: Record<string, string> = { | ||
token: getTokenFromClientKey(clientKey), | ||
...(styles?.fontFamily && { font: styles?.fontFamily }), | ||
...(styles?.stylesheetUrl && { stylesheet: styles?.stylesheetUrl }), | ||
}; | ||
|
||
const iFrameSrc = `${baseUrl}?${new URLSearchParams(params).toString()}`; | ||
|
||
/** | ||
* Adds an event listener to the window to listen to messages from the iframe. | ||
*/ | ||
React.useEffect(() => { | ||
const iFrameEventListener = getIFrameEventListener(baseUrl.origin, { | ||
setIFrameHeight, | ||
onValidateSuccess, | ||
onValidateFailure, | ||
onCreateCardForTemporaryUseSuccess, | ||
onCreateCardForTemporaryUseFailure, | ||
}); | ||
window.addEventListener("message", iFrameEventListener); | ||
return () => window.removeEventListener("message", iFrameEventListener); | ||
}, []); | ||
|
||
function sendMessageToStoreCardForTemporaryUse() { | ||
if (!iFrameReference.current) { | ||
throw new Error( | ||
"Attempted to call `sendMessageToStoreCardForTemporaryUse` with empty iFrameReference" | ||
); | ||
} | ||
|
||
const iFrame = iFrameReference.current; | ||
if (!iFrame.contentWindow) { | ||
throw new Error( | ||
"Attempted to call `sendMessageToStoreCardForTemporaryUse` but the iFrame contentWindow is null" | ||
); | ||
} | ||
|
||
iFrame.contentWindow.postMessage( | ||
{ type: "create-card-for-temporary-use" }, | ||
baseUrl.origin | ||
); | ||
} | ||
|
||
/** | ||
* useEffect to react to changes on the actions prop. | ||
*/ | ||
React.useEffect(() => { | ||
if (actions.includes("create-card-for-temporary-use")) { | ||
sendMessageToStoreCardForTemporaryUse(); | ||
} | ||
}, [actions]); | ||
|
||
return ( | ||
<iframe | ||
ref={iFrameReference} | ||
title="Card Payment Form" | ||
src={iFrameSrc} | ||
style={{ | ||
width: "100%", | ||
border: "none", | ||
height: iFrameHeight, | ||
}} | ||
/> | ||
); | ||
}; |
57 changes: 57 additions & 0 deletions
57
src/components/DuffelCardForm/lib/getIFrameEventListener.ts
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
import { DuffelCardFormProps } from "./types"; | ||
|
||
type Inputs = { | ||
setIFrameHeight: (height: string) => void; | ||
} & Pick< | ||
DuffelCardFormProps, | ||
| "onValidateSuccess" | ||
| "onValidateFailure" | ||
| "onCreateCardForTemporaryUseSuccess" | ||
| "onCreateCardForTemporaryUseFailure" | ||
>; | ||
|
||
export function getIFrameEventListener( | ||
baseUrl: string, | ||
{ | ||
setIFrameHeight, | ||
onValidateSuccess, | ||
onValidateFailure, | ||
onCreateCardForTemporaryUseSuccess, | ||
onCreateCardForTemporaryUseFailure, | ||
}: Inputs | ||
) { | ||
return function iFrameEventListener(event: MessageEvent) { | ||
if (!baseUrl?.startsWith(event.origin) || !event.data || !event.data.type) { | ||
return; | ||
} | ||
|
||
const eventType = event?.data?.type; | ||
|
||
switch (eventType) { | ||
case "iframe-loaded": | ||
setIFrameHeight(event.data.height); | ||
return; | ||
|
||
case "validate-success": | ||
onValidateSuccess(); | ||
return; | ||
|
||
case "validate-failure": | ||
onValidateFailure(); | ||
return; | ||
|
||
case "create-card-for-temporary-use-success": | ||
onCreateCardForTemporaryUseSuccess(event.data.data); | ||
return; | ||
|
||
case "create-card-for-temporary-use-failure": | ||
onCreateCardForTemporaryUseFailure(event.data.error); | ||
return; | ||
|
||
default: | ||
// eslint-disable-next-line | ||
console.log(`Unknown event type: ${eventType}`); | ||
return; | ||
} | ||
}; | ||
} |
18 changes: 18 additions & 0 deletions
18
src/components/DuffelCardForm/lib/getTokenFromClientKey.ts
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
export function getTokenFromClientKey(clientKey: string): string { | ||
if (clientKey.split(".").length !== 3) { | ||
throw new Error( | ||
"Invalid clientKey attribute in DuffelCardForm. It must be a valid JWT." | ||
); | ||
} | ||
|
||
const payloadString = clientKey.split(".")[1]; | ||
|
||
try { | ||
const payload = JSON.parse(atob(payloadString)); | ||
return payload.token; | ||
} catch (error) { | ||
throw new Error( | ||
"Invalid clientKey attribute in DuffelCardForm. It was not possible to read the payload." | ||
); | ||
} | ||
} |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
import { CustomStyles } from "src/types"; | ||
|
||
export interface CreateCardForTemporaryUseData { | ||
id: string; | ||
live_mode: false; | ||
} | ||
|
||
export interface CreateCardForTemporaryUseError { | ||
status: number; | ||
message: string; | ||
} | ||
|
||
export type DuffelCardFormStyles = Pick<CustomStyles, "fontFamily"> & { | ||
stylesheetUrl?: string; | ||
}; | ||
|
||
export type DuffelCardFormActions = | ||
| "validate" | ||
| "create-card-for-temporary-use"; | ||
|
||
export interface DuffelCardFormProps { | ||
/** | ||
* The client key present in the Quote object. | ||
*/ | ||
clientKey: string; | ||
|
||
/** | ||
* The styles to apply to the iframe input elements. | ||
*/ | ||
styles?: DuffelCardFormStyles; | ||
|
||
/** | ||
* If you want to develop with a local deployment of the token proxy on port 8000. Set this flag to true. | ||
*/ | ||
shouldUseLocalTokenProxy?: boolean; | ||
|
||
/** | ||
* The actions you'd like the component to perform. | ||
* | ||
* This prop is a dependecy of a useEffect hook in the component | ||
* and so when it's changed it will perform the action you specify. | ||
* | ||
* The action `create-card-for-temporary-use` will only happen once `validate` has been successful. | ||
* | ||
*/ | ||
actions: DuffelCardFormActions[]; | ||
|
||
/** | ||
* This function will be called when the card form validation has been successful. | ||
*/ | ||
onValidateSuccess: () => void; | ||
|
||
/** | ||
* If the card form validation is successful but data is changed afterwards, | ||
* making it invalid, this function will be called. | ||
*/ | ||
onValidateFailure: () => void; | ||
|
||
/** | ||
* This function will be called when the card has been created for temporary use. | ||
* | ||
* This callback will only be triggered if the `create-card-for-temporary-use` | ||
* action is present in the `actions` prop. | ||
*/ | ||
onCreateCardForTemporaryUseSuccess: ( | ||
data: CreateCardForTemporaryUseData | ||
) => void; | ||
|
||
/** | ||
* This function will be called when the component has failed to create the card for temporary use. | ||
* | ||
* This callback will only be triggered if the `create-card-for-temporary-use` | ||
* action is present in the `actions` prop. | ||
*/ | ||
onCreateCardForTemporaryUseFailure: ( | ||
error: CreateCardForTemporaryUseError | ||
) => void; | ||
} |
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
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
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
import { DuffelCardForm } from "@components/DuffelCardForm/DuffelCardForm"; | ||
import { DuffelCardFormProps } from "@components/DuffelCardForm/lib/types"; | ||
import type { Meta, StoryObj } from "@storybook/react"; | ||
|
||
export default { | ||
title: "DuffelCardForm", | ||
component: DuffelCardForm, | ||
} as Meta; | ||
|
||
type DuffelCardFormStory = StoryObj<typeof DuffelCardForm>; | ||
|
||
const defaultProps: DuffelCardFormProps = { | ||
clientKey: | ||
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbiI6IkVOQ1JZUFRFRF9UT0tFTiJ9.qP-zHSkMn-O9VGSj4wkh_A4VDOIrzpgRxh1xgLZ51rk", | ||
shouldUseLocalTokenProxy: true, | ||
actions: ["validate"], | ||
onValidateSuccess: console.log, | ||
onValidateFailure: console.error, | ||
onCreateCardForTemporaryUseSuccess: console.log, | ||
onCreateCardForTemporaryUseFailure: console.error, | ||
}; | ||
|
||
export const Default: DuffelCardFormStory = { args: defaultProps }; | ||
|
||
export const WithFont: DuffelCardFormStory = { | ||
args: { | ||
...defaultProps, | ||
styles: { | ||
fontFamily: `'Tangerine', serif`, | ||
stylesheetUrl: "https://fonts.googleapis.com/css?family=Tangerine", | ||
}, | ||
}, | ||
}; |
File renamed without changes.