Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions __tests__/CardUtils.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import {
formatCard,
getCardType,
getIcon,
isValidCard,
cvvLength,
isValidCvv,
isValidDate,
} from "../src/utils/card";

describe("card utils", () => {
it("formats and validates card number", () => {
expect(formatCard("4242424242424242")).toBe("4242 4242 4242 4242");
expect(isValidCard("4242 4242 4242 4242")).toBe(true);
});

it("detects card type including extended whitelist (Discover)", () => {
expect(getCardType("6011111111111117")).toBe("Discover");
});

it("cvvLength varies by type (Amex -> 4)", () => {
// Amex test number
const amex = "378282246310005";
expect(cvvLength(amex)).toBe(4);
expect(cvvLength("4242 4242 4242 4242")).toBe(3);
});

it("validates cvv by inferred type", () => {
// Amex 4-digit CVC (It's an AMEX Test card number)
expect(isValidCvv("1234", "345678901234564")).toBe(true);
// Visa 3-digit CVC
expect(isValidCvv("123", "4242 4242 4242 4242")).toBe(true);
});

it("getIcon returns undefined for unknown type", () => {
// @ts-ignore
expect(getIcon("UnknownBrand")).toBeUndefined();
});

it("isValidDate rejects incomplete or past dates", () => {
expect(isValidDate("06/")).toBe(false);
});
});
30 changes: 30 additions & 0 deletions __tests__/FramesContextGuards.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import React from "react";
import { render } from "@testing-library/react-native";

import { CardNumber, ExpiryDate, Cvv, SubmitButton } from "../src/index";

describe("Component context guards", () => {
it("throws when CardNumber is rendered outside Frames", () => {
expect(() => render(<CardNumber />)).toThrow(
"It looks like you are trying to render the CardNumber outside of the Frames Component."
);
});

it("throws when ExpiryDate is rendered outside Frames", () => {
expect(() => render(<ExpiryDate />)).toThrow(
"It looks like you are trying to render the ExpiryDate outside of the Frames Component."
);
});

it("throws when Cvv is rendered outside Frames", () => {
expect(() => render(<Cvv />)).toThrow(
"It looks like you are trying to render the Cvv outside of the Frames Component."
);
});

it("throws when SubmitButton is rendered outside Frames", () => {
expect(() => render(<SubmitButton title="Pay" />)).toThrow(
"It looks like you are trying to render the SubmitButton outside of the Frames Component."
);
});
});
204 changes: 204 additions & 0 deletions __tests__/HttpAndLogger.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
import React from "react";
import { render, fireEvent, waitFor } from "@testing-library/react-native";

import {
Frames,
CardNumber,
ExpiryDate,
Cvv,
SubmitButton,
} from "../src/index";

import { SANDBOX_LOGGER, LIVE_LOGGER } from "../src/utils/constants";

describe("http.tokenize and helpers", () => {
const { tokenize, formatDataForTokenization, getEnvironment } =
jest.requireActual("../src/utils/http");

beforeEach(() => {
// @ts-ignore
global.fetch = jest.fn();
});

it("tokenize returns json when response ok", async () => {
// @ts-ignore
global.fetch.mockResolvedValueOnce({
ok: true,
json: jest.fn().mockResolvedValue({ ok: true }),
});

const res = await tokenize({
key: "pk_sbox_dummy",
body: {
type: "card",
number: "4242424242424242",
expiry_month: "12",
expiry_year: "2030",
cvv: "123",
},
});

expect(res).toEqual({ ok: true });
});

it("tokenize throws json when response not ok", async () => {
// @ts-ignore
global.fetch.mockResolvedValueOnce({
ok: false,
json: jest.fn().mockResolvedValue({ error: "bad" }),
});

await expect(
tokenize({
key: "pk_sbox_dummy",
body: {
type: "card",
number: "4242424242424242",
expiry_month: "12",
expiry_year: "2030",
cvv: "123",
},
})
).rejects.toEqual({ error: "bad" });
});

it("formatDataForTokenization maps fields and billing address including addressLine2", () => {
const { EXPIRY_DATE_DELIMITER } = require("../src/utils/constants");
const state = {
cardNumber: "4242 4242 4242 4242",
cardBin: { bin: null, scheme: null },
cardIcon: undefined,
cardType: null,
expiryDate: `12${EXPIRY_DATE_DELIMITER}30`,
cvv: "123",
cvvLength: 3,
validation: { cardNumber: true, expiryDate: true, cvv: true, card: true },
};

const config = {
publicKey: "pk_sbox_dummy",
cardholder: {
name: "John Doe",
phone: "+1234567890",
billingAddress: {
addressLine1: "Line 1",
addressLine2: "Line 2",
city: "City",
state: "ST",
zip: "12345",
country: "US",
},
},
};

const params = formatDataForTokenization(state, config);
expect(params.key).toBe(config.publicKey);
expect(params.body.number).toBe("4242424242424242");
expect(params.body.expiry_month).toBe("12");
expect(params.body.expiry_year).toMatch(/^20\d{2}$/);
expect(params.body.cvv).toBe("123");
expect(params.body.name).toBe("John Doe");
expect(params.body.billing_address).toEqual({
address_line1: "Line 1",
address_line2: "Line 2",
city: "City",
state: "ST",
zip: "12345",
country: "US",
});
expect(params.body.phone).toEqual({ number: "+1234567890" });
});

it("formatDataForTokenization sets billing_address to null when not provided", () => {
const state = {
cardNumber: "4242 4242 4242 4242",
cardBin: { bin: null, scheme: null },
cardIcon: undefined,
cardType: null,
expiryDate: "12/30",
cvv: "123",
cvvLength: 3,
validation: { cardNumber: true, expiryDate: true, cvv: true, card: true },
};

const config = {
publicKey: "pk_sbox_dummy",
cardholder: { name: "John Doe" },
};

const params = formatDataForTokenization(state, config);
// @ts-ignore
expect(params.body.billing_address).toBeNull();
});

it("getEnvironment returns expected token URL", () => {
expect(getEnvironment("pk_sbox_dummy")).toBe(
"https://api.sandbox.checkout.com/tokens"
);
});
});

describe("logger.log", () => {
const { log, getEnvironment } = require("../src/utils/logger");

beforeEach(() => {
// @ts-ignore
global.fetch = jest.fn().mockResolvedValue({});
});

it("posts to sandbox logger with structured body", async () => {
await log("info", "type_event", { publicKey: "pk_sbox_dummy" }, { a: 1 });

expect(global.fetch).toHaveBeenCalledWith(
SANDBOX_LOGGER,
expect.anything()
);
const [, options] = (global.fetch as jest.Mock).mock.calls[0];
const body = JSON.parse(options.body);
expect(body.specversion).toBe("1.0");
expect(body.type).toBe("type_event");
expect(body.data.environment).toBe("sandbox");
expect(body.cko.loglevel).toBe("info");
});

it("getEnvironment returns live logger for prod keys", () => {
expect(getEnvironment("pk_4296fd52-efba-4a38-b6ce-cf0d93639d8a")).toBe(
LIVE_LOGGER
);
});
});

describe("Frames error path on tokenization failure", () => {
it("emits cardTokenizationFailed when tokenization rejects", async () => {
const httpMod = require("../src/utils/http");
jest.spyOn(httpMod, "tokenize").mockRejectedValueOnce({ error: "failure" });

const { Frames } = require("../src/index");

const cardTokenizationFailed = jest.fn();

const screen = render(
<Frames
config={{ publicKey: "pk_sbox_dummy", debug: true }}
cardTokenizationFailed={cardTokenizationFailed}
>
<CardNumber />
<ExpiryDate />
<Cvv />
<SubmitButton title="Pay" />
</Frames>
);

fireEvent.changeText(
screen.getByPlaceholderText("•••• •••• •••• ••••"),
"4242 4242 4242 4242"
);
fireEvent.changeText(screen.getByPlaceholderText("MM/YY"), "12/30");
fireEvent.changeText(screen.getByPlaceholderText("•••"), "123");

fireEvent.press(screen.getByText("Pay"));

await waitFor(() => expect(cardTokenizationFailed).toHaveBeenCalled());
expect(cardTokenizationFailed).toHaveBeenCalledWith({ error: "failure" });
});
});
50 changes: 46 additions & 4 deletions __tests__/Integration.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@ describe("Frames integration", () => {
type: "card",
token: "tok_test_123",
expires_on: "2030-12-31T23:59:59Z",
expiry_month: "12",
expiry_year: "2030",
expiry_month: 12,
expiry_year: 2030,
scheme: "Visa",
last4: "4242",
bin: "424242",
Expand Down Expand Up @@ -147,8 +147,8 @@ describe("Frames integration", () => {
type: "card",
token: "tok_test_ref_123",
expires_on: "2030-12-31T23:59:59Z",
expiry_month: "12",
expiry_year: "2030",
expiry_month: 12,
expiry_year: 2030,
scheme: "Visa",
last4: "4242",
bin: "424242",
Expand Down Expand Up @@ -187,4 +187,46 @@ describe("Frames integration", () => {
await waitFor(() => expect(tokenize).toHaveBeenCalledTimes(1));
expect(cardTokenized).toHaveBeenCalledWith(mockTokenResponse);
});

it("SubmitButton triggers submitCard and forwards onPress to consumer", async () => {
const { tokenize } = require("../src/utils/http");

const mockTokenResponse = {
type: "card",
token: "tok_test_press_123",
expires_on: "2030-12-31T23:59:59Z",
expiry_month: 12,
expiry_year: 2030,
scheme: "Visa",
last4: "4242",
bin: "424242",
};

tokenize.mockResolvedValueOnce(mockTokenResponse);

const onPressSpy = jest.fn();
const cardTokenized = jest.fn();

const screen = render(
<Frames config={config} cardTokenized={cardTokenized}>
<CardNumber />
<ExpiryDate />
<Cvv />
<SubmitButton title="Pay" onPress={onPressSpy} />
</Frames>
);

fireEvent.changeText(
screen.getByPlaceholderText("•••• •••• •••• ••••"),
validVisa
);
fireEvent.changeText(screen.getByPlaceholderText("MM/YY"), validExpiry);
fireEvent.changeText(screen.getByPlaceholderText("•••"), validCvv);

fireEvent.press(screen.getByText("Pay"));

await waitFor(() => expect(tokenize).toHaveBeenCalledTimes(1));
expect(cardTokenized).toHaveBeenCalledWith(mockTokenResponse);
expect(onPressSpy).toHaveBeenCalledTimes(1);
});
});
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "frames-react-native",
"version": "1.2.3",
"version": "1.2.4",
"description": "Frames React Native",
"main": "dist/index.js",
"scripts": {
Expand Down
11 changes: 6 additions & 5 deletions src/components/CardNumber.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,16 @@ const CardNumber: React.FC<FramesCardFieldProps> = (props) => {
value={state.cardNumber ?? ""}
style={[styles.cardNumber, props.style]}
onChangeText={(val: string) => {
dispatch({ type: CARD_CHANGE, payload: val });
const safeValue = val ?? "";
dispatch({ type: CARD_CHANGE, payload: safeValue });
const digits = safeValue.replace(/[^0-9]/g, "");
if (
val.replace(/[^0-9]/g, "").length >= 8 &&
val.replace(/[^0-9]/g, "").substring(0, 8) !==
state.cardBin.bin
digits.length >= 8 &&
digits.substring(0, 8) !== state.cardBin.bin
) {
dispatch({
type: BIN_CHANGE,
payload: val.replace(/[^0-9]/g, "").substring(0, 8),
payload: digits.substring(0, 8),
});
}
}}
Expand Down
4 changes: 2 additions & 2 deletions src/types/types.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -170,8 +170,8 @@ export interface FrameCardTokenizedEvent {
type: string;
token: string;
expires_on: string;
expiry_month: string;
expiry_year: string;
expiry_month: number;
expiry_year: number;
scheme?: Scheme;
scheme_local?: string;
last4: string;
Expand Down
Loading