@merchi/cart 1.4.4 → 1.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,18 +1,102 @@
1
1
  import * as React from 'react';
2
+ import { useEffect, useState } from 'react';
2
3
  import { StripePaymentForm, StripePaymentGate } from '@merchi/invoice';
3
4
  import LegacyStripeCardForm from './LegacyStripeCardForm';
5
+ import { completeTestPayment, fetchTestPublishableKey } from './actions';
4
6
  import { useCartContext } from '../CartProvider';
5
7
  import { tabIdPaymentSuccess } from '../utilities/tabs';
8
+ import { TEST_CARD_NUMBER } from '../utilities/test_checkout';
9
+
10
+ const testCheckoutHint = (
11
+ <div style={{marginBottom: '12px'}}>
12
+ <div style={{color: 'red', fontWeight: 600}}>Test checkout</div>
13
+ <p style={{margin: '4px 0 0', fontSize: '13px'}}>
14
+ No real charge. Use card {TEST_CARD_NUMBER}, any future expiry, any CVC,
15
+ any ZIP.
16
+ </p>
17
+ </div>
18
+ );
6
19
 
7
20
  export default function StripeCardForm() {
8
- const { alertError, apiUrl, cart, setActiveTabIndex, setInvoiceJson } = useCartContext();
9
- return <StripePaymentGate engine={cart.stripePaymentEngine} apiUrl={apiUrl || 'https://api.merchi.co/v6/'} resource="cart" resourceId={cart.id}
10
- resourceToken={cart.token || cart.cartToken} legacy={<LegacyStripeCardForm />}>
11
- <StripePaymentForm apiUrl={apiUrl || 'https://api.merchi.co/v6/'} resource="cart"
12
- resourceId={cart.id} resourceToken={cart.token || cart.cartToken} onError={alertError}
13
- onSuccess={invoice => {
21
+ const {
22
+ alertError,
23
+ apiUrl,
24
+ cart,
25
+ classNameBtnPay,
26
+ setActiveTabIndex,
27
+ setInvoiceJson,
28
+ } = useCartContext();
29
+ const isTestCheckout = Boolean(cart.isTest);
30
+ const url = apiUrl || 'https://api.merchi.co/v6/';
31
+ const [testKeyMissing, setTestKeyMissing] = useState(false);
32
+ const [loadingTestPayment, setLoadingTestPayment] = useState(false);
33
+
34
+ useEffect(() => {
35
+ if (!isTestCheckout) {
36
+ setTestKeyMissing(false);
37
+ return;
38
+ }
39
+ fetchTestPublishableKey(url)
40
+ .then(() => setTestKeyMissing(false))
41
+ .catch(() => setTestKeyMissing(true));
42
+ }, [isTestCheckout, url]);
43
+
44
+ async function doCompleteTestPayment() {
45
+ setLoadingTestPayment(true);
46
+ try {
47
+ const invoice = await completeTestPayment(url, cart);
14
48
  setInvoiceJson(invoice);
15
49
  setActiveTabIndex(tabIdPaymentSuccess);
16
- }} />
17
- </StripePaymentGate>;
50
+ } catch (e: any) {
51
+ alertError(e.message);
52
+ } finally {
53
+ setLoadingTestPayment(false);
54
+ }
55
+ }
56
+
57
+ if (isTestCheckout && testKeyMissing) {
58
+ return (
59
+ <>
60
+ {testCheckoutHint}
61
+ <p style={{fontSize: '13px', marginBottom: '12px'}}>
62
+ Stripe test keys are not configured. Complete this as a test order
63
+ without a card.
64
+ </p>
65
+ <button
66
+ type='button'
67
+ className={classNameBtnPay}
68
+ disabled={loadingTestPayment}
69
+ onClick={doCompleteTestPayment}
70
+ >
71
+ {loadingTestPayment ? 'Completing…' : 'Complete test order'}
72
+ </button>
73
+ </>
74
+ );
75
+ }
76
+
77
+ return (
78
+ <>
79
+ {isTestCheckout && testCheckoutHint}
80
+ <StripePaymentGate
81
+ engine={isTestCheckout ? 'legacy' : cart.stripePaymentEngine}
82
+ apiUrl={url}
83
+ resource="cart"
84
+ resourceId={cart.id}
85
+ resourceToken={cart.token || cart.cartToken}
86
+ legacy={<LegacyStripeCardForm />}
87
+ >
88
+ <StripePaymentForm
89
+ apiUrl={url}
90
+ resource="cart"
91
+ resourceId={cart.id}
92
+ resourceToken={cart.token || cart.cartToken}
93
+ onError={alertError}
94
+ onSuccess={invoice => {
95
+ setInvoiceJson(invoice);
96
+ setActiveTabIndex(tabIdPaymentSuccess);
97
+ }}
98
+ />
99
+ </StripePaymentGate>
100
+ </>
101
+ );
18
102
  }
@@ -0,0 +1,50 @@
1
+ import { completeTestPayment, fetchTestPublishableKey } from './actions';
2
+
3
+ describe('test checkout stripe actions', () => {
4
+ const originalFetch = global.fetch;
5
+
6
+ afterEach(() => {
7
+ global.fetch = originalFetch;
8
+ });
9
+
10
+ it('loads the master test publishable key', async () => {
11
+ global.fetch = jest.fn().mockResolvedValue({
12
+ ok: true,
13
+ json: async () => ({ key: 'pk_test_master' }),
14
+ }) as any;
15
+
16
+ await expect(fetchTestPublishableKey('https://api.test/v6/')).resolves.toBe(
17
+ 'pk_test_master'
18
+ );
19
+ expect(global.fetch).toHaveBeenCalledWith(
20
+ 'https://api.test/v6/stripe/test_publishable_key/',
21
+ { method: 'GET', mode: 'cors' }
22
+ );
23
+ });
24
+
25
+ it('fails over when the test-key request is rejected', async () => {
26
+ global.fetch = jest.fn().mockResolvedValue({
27
+ ok: false,
28
+ json: async () => ({ message: 'missing' }),
29
+ }) as any;
30
+
31
+ await expect(fetchTestPublishableKey('https://api.test/v6/')).rejects.toThrow(
32
+ 'Stripe test keys are not configured'
33
+ );
34
+ });
35
+
36
+ it('completes a no-Stripe test payment with the cart token', async () => {
37
+ global.fetch = jest.fn().mockResolvedValue({
38
+ ok: true,
39
+ json: async () => ({ invoice: { id: 9, isTest: true } }),
40
+ }) as any;
41
+
42
+ await expect(
43
+ completeTestPayment('https://api.test/v6/', { id: 4, token: 'tok' })
44
+ ).resolves.toEqual({ id: 9, isTest: true });
45
+ expect(global.fetch).toHaveBeenCalledWith(
46
+ 'https://api.test/v6/carts/4/complete_test_payment/?cart_token=tok',
47
+ { method: 'POST', mode: 'cors' }
48
+ );
49
+ });
50
+ });
@@ -7,6 +7,47 @@ export async function stripeInitPromise(apiUrl: string) {
7
7
  ).then((response: any) => response.key);
8
8
  }
9
9
 
10
+ export async function fetchTestPublishableKey(apiUrl: string) {
11
+ const response: any = await fetch(
12
+ `${apiUrl}stripe/test_publishable_key/`,
13
+ { method: 'GET', mode: 'cors' }
14
+ );
15
+ if (response && typeof response.json === 'function') {
16
+ if (!response.ok) {
17
+ throw new Error('Stripe test keys are not configured');
18
+ }
19
+ const data = await response.json();
20
+ if (!data || !data.key) {
21
+ throw new Error('Stripe test keys are not configured');
22
+ }
23
+ return data.key;
24
+ }
25
+ if (!response || !response.key) {
26
+ throw new Error('Stripe test keys are not configured');
27
+ }
28
+ return response.key;
29
+ }
30
+
31
+ export async function completeTestPayment(apiUrl: string, cart: any) {
32
+ const query = new URLSearchParams({ cart_token: cart.token }).toString();
33
+ const response = await fetch(
34
+ `${apiUrl}carts/${cart.id}/complete_test_payment/?${query}`,
35
+ { method: 'POST', mode: 'cors' }
36
+ );
37
+ if (!response.ok) {
38
+ let message = 'Unable to complete the test order.';
39
+ try {
40
+ const data = await response.json();
41
+ message = data.message || data.errorMessage || message;
42
+ } catch {
43
+ // keep default
44
+ }
45
+ throw new Error(message);
46
+ }
47
+ const data = await response.json();
48
+ return data.invoice;
49
+ }
50
+
10
51
  async function createPaymentIntent(apiUrl: string, cart: any, paymentMethodType?: string) {
11
52
  const { id, token } = cart;
12
53
  const fetchOptions: any = {
@@ -1,4 +1,5 @@
1
1
  import { isBrowser } from "browser-or-node";
2
+ import { cartCookieName, readTestCheckoutFlag } from "./test_checkout";
2
3
 
3
4
  export async function getCookie(name: string, defaultValue: any) {
4
5
  if (!isBrowser) {
@@ -22,13 +23,14 @@ export async function getCookie(name: string, defaultValue: any) {
22
23
  }
23
24
  }
24
25
 
25
- export async function getCartCookie(domainId: number | string) {
26
- const idAndToken: string = await getCookie(`cart-${domainId}`, null);
26
+ export async function getCartCookie(domainId: number | string, isTest?: boolean) {
27
+ const test = isTest ?? readTestCheckoutFlag();
28
+ const idAndToken: string = await getCookie(cartCookieName(domainId, test), null);
27
29
  return idAndToken ? idAndToken.split(',') : null;
28
30
  }
29
31
 
30
- export async function getCartCookieToken(domainId: number | string) {
31
- const cartIdAndToken = domainId ? await getCartCookie(Number(domainId)) : undefined;
32
+ export async function getCartCookieToken(domainId: number | string, isTest?: boolean) {
33
+ const cartIdAndToken = domainId ? await getCartCookie(Number(domainId), isTest) : undefined;
32
34
  return cartIdAndToken && cartIdAndToken[1] || undefined;
33
35
  }
34
36
 
@@ -52,7 +54,8 @@ async function setSessionCookie(name: string, value: any, domain?: any) {
52
54
  document.cookie = cookie;
53
55
  }
54
56
 
55
- export const setCartCookie = async (storeId: number, cart: any, domain?: any) => {
57
+ export const setCartCookie = async (storeId: number, cart: any, domain?: any, isTest?: boolean) => {
58
+ const test = isTest ?? (cart ? Boolean(cart.isTest) : readTestCheckoutFlag());
56
59
  const cookieValue = cart ? cart.id + ',' + cart.token : '';
57
- await setSessionCookie('cart-' + String(storeId), cookieValue, domain);
60
+ await setSessionCookie(cartCookieName(storeId, test), cookieValue, domain);
58
61
  };
@@ -10,5 +10,6 @@ export * from './local_storage';
10
10
  export * from './product';
11
11
  export * from './shipment';
12
12
  export * from './tabs';
13
+ export * from './test_checkout';
13
14
  export * from './user';
14
15
  export * from './variations';
@@ -0,0 +1,39 @@
1
+ import {
2
+ TEST_CARD_NUMBER,
3
+ TEST_CHECKOUT_QUERY,
4
+ TEST_CHECKOUT_STORAGE_KEY,
5
+ cartCookieName,
6
+ readTestCheckoutFlag,
7
+ } from './test_checkout';
8
+
9
+ describe('test checkout helpers', () => {
10
+ const originalLocation = window.location;
11
+
12
+ beforeEach(() => {
13
+ window.localStorage.clear();
14
+ delete (window as any).location;
15
+ (window as any).location = { search: '' };
16
+ });
17
+
18
+ afterEach(() => {
19
+ (window as any).location = originalLocation;
20
+ window.localStorage.clear();
21
+ });
22
+
23
+ it('builds a separate cookie name for test carts', () => {
24
+ expect(cartCookieName(12)).toBe('cart-12');
25
+ expect(cartCookieName(12, true)).toBe('cart-12-test');
26
+ });
27
+
28
+ it('persists the query flag and exposes the test card number', () => {
29
+ (window as any).location = { search: `?${TEST_CHECKOUT_QUERY}=1` };
30
+ expect(readTestCheckoutFlag()).toBe(true);
31
+ expect(window.localStorage.getItem(TEST_CHECKOUT_STORAGE_KEY)).toBe('1');
32
+ expect(TEST_CARD_NUMBER).toBe('4242 4242 4242 4242');
33
+ });
34
+
35
+ it('reads the stored flag when the query is absent', () => {
36
+ window.localStorage.setItem(TEST_CHECKOUT_STORAGE_KEY, '1');
37
+ expect(readTestCheckoutFlag()).toBe(true);
38
+ });
39
+ });
@@ -0,0 +1,27 @@
1
+ export const TEST_CHECKOUT_QUERY = 'merchi_test_checkout';
2
+ export const TEST_CHECKOUT_STORAGE_KEY = 'merchi_test_checkout';
3
+ export const TEST_CARD_NUMBER = '4242 4242 4242 4242';
4
+
5
+ export function readTestCheckoutFlag(): boolean {
6
+ if (typeof window === 'undefined') {
7
+ return false;
8
+ }
9
+ try {
10
+ const params = new URLSearchParams(window.location.search);
11
+ if (params.get(TEST_CHECKOUT_QUERY) === '1') {
12
+ window.localStorage.setItem(TEST_CHECKOUT_STORAGE_KEY, '1');
13
+ return true;
14
+ }
15
+ return window.localStorage.getItem(TEST_CHECKOUT_STORAGE_KEY) === '1';
16
+ } catch {
17
+ return false;
18
+ }
19
+ }
20
+
21
+ export function persistTestCheckoutFlag(): boolean {
22
+ return readTestCheckoutFlag();
23
+ }
24
+
25
+ export function cartCookieName(domainId: number | string, isTest = false): string {
26
+ return isTest ? `cart-${domainId}-test` : `cart-${domainId}`;
27
+ }