Collect cards.
In this guide, we'll collect card numbers from your customers and put them into the vault for future use. (e.g. to either make a payment or reveal it to the customer.)
PCI Compliance
The most important thing is that you, your app, your company, or your employees don't see or touch that data, it is the only way you can reduce your PCI scope to the minimum, more on this here. Therefore we'll use our SDKs which provide isolated iFrames.
No one except for the card issuer is allowed to store CVCs, CVV, PIN, or other Card Authorization Data for longer than it’s required to make a payment.
Demo - What we'll be building
Requirements
- A workspace setup with Modernbanc.
- A valid API key with a role that has
create
secret permissions. This will be used in the SDK to turn collected data into Secrets.
Adding Modernbanc Inputs to your app.
To minimize PCI scope your app should never touch or access the card data. To achieve that you need to embed Modernbanc Input components to your screen/page.
They're isolated, headless, and flexible so you can build any kind of user journey and safely pass the data through to our Vault without exposing it to your app.
For this example we'll use our React SDK - but the same principles apply to our iOS and Android SDKs.
import { ModernbancProvider } from '@modernbanc/react';
import View from './form/form.comp';
const App = () => (
<ModernbancProvider api_key="YOUR_API_KEY">
<View />
</ModernbancProvider>
);
export default App;
// View Component
import { InputElement, useModernbancContext } from "@modernbanc/react";
import { useState } from "react";
const View = () => {
const [loading, setLoading] = useState<boolean>(false);
const [success, setSuccess] = useState<boolean>();
const [valid_card_number, setValidCardNumber] = useState<boolean>(false);
const [valid_expiry_date, setValidExpiryDate] = useState<boolean>(false);
const [valid_cvc, setValidCvc] = useState<boolean>(false);
const [number_input_is_ready, setNumberInputIsReady] = useState(false);
const [expiry_date_input_is_ready, setExpiryDateInputIsReady] = useState(false);
const [cvc_input_is_ready, setCvcInputIsReady] = useState(false);
const all_inputs_are_ready = number_input_is_ready && expiry_date_input_is_ready && cvc_input_is_ready;
const context = useModernbancContext();
const show_form = all_inputs_are_ready && success === undefined && !loading;
const can_submit = valid_card_number && valid_expiry_date && valid_cvc;
const css = `padding: 5px; border: 1px solid #ccc; border-radius: 5px`;
return (<></>);
};
// View Component
// rest of the code
return (
<div
style={{
display: "flex",
flexDirection: "column",
gap: "10px",
color: "gray",
width: "400px",
height: "500px",
}}
>
{loading && <>Loading</>}
{success && <>Success</>}
<form
style={{
display: show_form ? "flex" : "none",
flexDirection: "column",
gap: "15px",
}}
>
<InputElement
id="number"
placeholder="0000 0000 0000 0000"
container={{ style: { height: "28px", width: "100%" } }}
onChange={({ data }) => {
const valid_card_number = data.matches?.valid_card_number;
setValidCardNumber(!!valid_card_number);
}}
css={css}
replacers={[{ type: "card_number" }]}
matchers={[{ type: "valid_card_number" }]}
onReady={() => {
setNumberInputIsReady(true);
}}
/>
<InputElement
id="exp-date"
placeholder="MM/YY"
container={{ style: { height: "28px", width: "100%" } }}
replacers={[{ type: "expiry_date" }]}
matchers={[{ type: "valid_expiry_date" }]}
onChange={(data) => {
setValidExpiryDate(!!data.data.matches?.["valid_expiry_date"]);
}}
css={css}
onReady={() => setExpiryDateInputIsReady(true)}
/>
<InputElement
id="cvc"
placeholder="CVC"
container={{ style: { height: "28px", width: "100%" } }}
onReady={() => setCvcInputIsReady(true)}
matchers={[
{ type: "regex", pattern: "^[0-9]{3,4}$", name: "valid_cvc" },
]}
onChange={(data) => {
setValidCvc(!!data.data.matches?.["valid_cvc"]);
}}
css={css}
/>
<button disabled={!can_submit} onClick={submit}>
Submit
</button>
</form>
</div>
);
// View Component
async function submit(e: React.MouseEvent<HTMLButtonElement, MouseEvent>) {
e.preventDefault();
setLoading(true);
// This will create a token from each input's value.
const number_promise = context.modernbanc
.getElement("input", "number")
?.createSecret();
const exp_date_promise = context.modernbanc
.getElement("input", "exp-date")
?.createSecret();
const cvc_promise = context.modernbanc
.getElement("input", "cvc")
?.createSecret();
const promises = [number_promise, exp_date_promise, cvc_promise];
const [number_result, exp_date_result, cvc_result] =
await Promise.all(promises);
console.log("Backend result", number_result);
if (
number_result?.is_error ||
cvc_result?.is_error ||
exp_date_result?.is_error
) {
console.log("Backend error", number_result, exp_date_result, cvc_result);
setLoading(false);
return;
}
const number_secret_id = number_result?.secret_id;
const exp_date_secret_id = exp_date_result?.secret_id;
const cvc_secret_id = cvc_result?.secret_id;
// Use these secret ids to make a payment or reveal the card number back to the user.
setSuccess(true);
setLoading(false);
}
// View Component
// View Component
import { InputElement, useModernbancContext } from "@modernbanc/react";
import { useState } from "react";
const View = () => {
const [loading, setLoading] = useState<boolean>(false);
const [success, setSuccess] = useState<boolean>();
const [valid_card_number, setValidCardNumber] = useState<boolean>(false);
const [valid_expiry_date, setValidExpiryDate] = useState<boolean>(false);
const [valid_cvc, setValidCvc] = useState<boolean>(false);
const [number_input_is_ready, setNumberInputIsReady] = useState(false);
const [expiry_date_input_is_ready, setExpiryDateInputIsReady] = useState(false);
const [cvc_input_is_ready, setCvcInputIsReady] = useState(false);
const all_inputs_are_ready = number_input_is_ready && expiry_date_input_is_ready && cvc_input_is_ready;
const context = useModernbancContext();
const show_form = all_inputs_are_ready && success === undefined && !loading;
const can_submit = valid_card_number && valid_expiry_date && valid_cvc;
const css = `padding: 5px; border: 1px solid #ccc; border-radius: 5px`;
// View Component
async function submit(e: React.MouseEvent<HTMLButtonElement, MouseEvent>) {
e.preventDefault();
setLoading(true);
// This will create token from each input's value.
const number_promise = context.modernbanc
.getElement("input", "number")
?.createSecret();
const exp_date_promise = context.modernbanc
.getElement("input", "exp-date")
?.createSecret();
const cvc_promise = context.modernbanc
.getElement("input", "cvc")
?.createSecret();
const promises = [number_promise, exp_date_promise, cvc_promise];
const [number_result, exp_date_result, cvc_result] =
await Promise.all(promises);
console.log("Backend result", number_result);
if (
number_result?.is_error ||
cvc_result?.is_error ||
exp_date_result?.is_error
) {
console.log("Backend error", number_result, exp_date_result, cvc_result);
setLoading(false);
return;
}
const number_secret_id = number_result?.secret_id;
const exp_date_secret_id = exp_date_result?.secret_id;
const cvc_secret_id = cvc_result?.secret_id;
// Use these secret ids to make a payment or reveal the card number back to the user.
setSuccess(true);
setLoading(false);
}
return (
<div
style={{
display: "flex",
flexDirection: "column",
gap: "10px",
color: "gray",
width: "400px",
height: "500px",
}}
>
{loading && <>Loading</>}
{success && <>Success</>}
<form
style={{
display: show_form ? "flex" : "none",
flexDirection: "column",
gap: "15px",
}}
>
<InputElement
id="number"
placeholder="0000 0000 0000 0000"
container={{ style: { height: "28px", width: "100%" } }}
onChange={({ data }) => {
const valid_card_number = data.matches?.valid_card_number;
setValidCardNumber(!!valid_card_number);
}}
css={css}
replacers={[{ type: "card_number" }]}
matchers={[{ type: "valid_card_number" }]}
onReady={() => {
setNumberInputIsReady(true);
}}
/>
<InputElement
id="exp-date"
placeholder="MM/YY"
container={{ style: { height: "28px", width: "100%" } }}
replacers={[{ type: "expiry_date" }]}
matchers={[{ type: "valid_expiry_date" }]}
onChange={(data) => {
setValidExpiryDate(!!data.data.matches?.["valid_expiry_date"]);
}}
css={css}
onReady={() => setExpiryDateInputIsReady(true)}
/>
<InputElement
id="cvc"
placeholder="CVC"
container={{ style: { height: "28px", width: "100%" } }}
onReady={() => setCvcInputIsReady(true)}
matchers={[
{ type: "regex", pattern: "^[0-9]{3,4}$", name: "valid_cvc" },
]}
onChange={(data) => {
setValidCvc(!!data.data.matches?.["valid_cvc"]);
}}
css={css}
/>
<button disabled={!can_submit} onClick={submit}>
Submit
</button>
</form>
</div>
);
};
export default View;
If you want to show a loading indicator during the secret creation don't dismount the inputs, hide them instead (e.g opacity: 0
).
if you dismount them the underlying iFrame will close and you won't get a response message back.
Demo Source Code
That's it!
Now when user types in their card number and clicks the submit button our SDK will upload it to the Vault and give you back an id of the secret in Modernbanc Vault.
In the next guide, we'll make use of that secret to either make a payment or reveal it to the customer.