merchi_cart 1.3.2 → 1.3.4

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.
@@ -15,6 +15,11 @@ import { makeAddress } from './utilities/address';
15
15
  import { appendStyleSheetText } from './utilities/helpers';
16
16
  import { makeJob } from './utilities/job';
17
17
  import { makeProduct } from './utilities/product';
18
+ import {
19
+ getSavedCheckoutCustomer,
20
+ saveCheckoutAddress,
21
+ } from './utilities/local_storage';
22
+ import { makeUser } from './utilities/user';
18
23
 
19
24
  export interface PropsCart {
20
25
  cart: any;
@@ -577,15 +582,44 @@ const CartProvider = ({
577
582
  const [fetchingCart, setFetchingCart] = useState(false);
578
583
  const [cartSettingsInvalid, setCartSettingsInvalid] = useState(false);
579
584
 
585
+ async function attachSavedClientIfNeeded(cartJson: any) {
586
+ if (cartJson?.client?.id) {
587
+ setCartClient(cartJson.client);
588
+ return cartJson;
589
+ }
590
+ const savedCustomer = getSavedCheckoutCustomer(domainId);
591
+ if (!savedCustomer?.id) {
592
+ setCartClient(null);
593
+ return cartJson;
594
+ }
595
+ try {
596
+ const cartToken =
597
+ cartJson.token || (await getCartCookieToken(domainId as number));
598
+ const clientEnt = makeUser({ id: savedCustomer.id }, true);
599
+ const cartEnt = makeCart({ ...cartJson }, false, cartToken);
600
+ cartEnt.client = clientEnt;
601
+ const savedCart = await cartEnt.save({ embed: cartEmbed });
602
+ const nextCart = savedCart.toJson();
603
+ if (nextCart?.client?.id) {
604
+ setCartClient(nextCart.client);
605
+ return nextCart;
606
+ }
607
+ } catch {
608
+ // Stale saved customer id — leave customer forms available.
609
+ }
610
+ setCartClient(null);
611
+ return cartJson;
612
+ }
613
+
580
614
  // Create cart and save cart cookie
581
615
  async function createCartAndCookie(callback?: () => void) {
582
616
  setFetchingCart(true);
583
617
  try {
584
618
  const cart = makeCart({domain: {id: domainId}}, true);
585
619
  await cart.create({embed: cartEmbed});
586
- const cartJson = await cart.toJson();
620
+ let cartJson = await cart.toJson();
587
621
  if (domainId) setCartCookie(Number(domainId), cartJson, undefined);
588
- setCartClient(null);
622
+ cartJson = await attachSavedClientIfNeeded(cartJson);
589
623
  setCart({...cartJson});
590
624
  if (callback) callback();
591
625
  } catch (e: any) {
@@ -603,8 +637,9 @@ const CartProvider = ({
603
637
  setFetchingCart(true);
604
638
  try {
605
639
  const cart = await merchi.Cart.get(id, {embed: cartEmbed});
606
- const cartJson = await cart.toJson();
640
+ let cartJson = await cart.toJson();
607
641
  if (stripeIsValidAndActive(cartJson)) {
642
+ cartJson = await attachSavedClientIfNeeded(cartJson);
608
643
  setCart({...cartJson});
609
644
  } else {
610
645
  console.error(
@@ -648,6 +683,7 @@ const CartProvider = ({
648
683
  cartEnt.receiverAddress = receiverAddress;
649
684
  cartEnt = await cartEnt.save({embed: cartEmbed});
650
685
  const cartJson = cartEnt.toJson();
686
+ saveCheckoutAddress(domainId, address);
651
687
  const query: Array<any> = [];
652
688
  query.push(['cart_token', cart.token]);
653
689
  const cartWithShipmentGroups = await merchi.authenticatedFetch(
@@ -10,6 +10,10 @@ import InputError from './InputError';
10
10
  import { createNewCustomer, makeUser } from '../utilities/user';
11
11
  import { makeCart } from '../utilities/cart';
12
12
  import { getCartCookieToken } from '../utilities/cookie';
13
+ import {
14
+ getSavedCheckoutCustomer,
15
+ saveCheckoutCustomer,
16
+ } from '../utilities/local_storage';
13
17
 
14
18
  export const emailValidation = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]/i;
15
19
 
@@ -27,6 +31,7 @@ export function FormCustomerNew() {
27
31
  const [loading, setLoading] = React.useState(false);
28
32
  const [error, setError] = React.useState(({} as any));
29
33
  const [acceptConditions, setAcceptConditions] = React.useState(true);
34
+ const savedCustomer = getSavedCheckoutCustomer(domainId);
30
35
 
31
36
  async function actionCreateNewCustomer(customerJson: any) {
32
37
  setError({});
@@ -50,6 +55,12 @@ export function FormCustomerNew() {
50
55
  if (!cartJson?.client?.id) {
51
56
  throw new Error('Unable to attach client to cart.');
52
57
  }
58
+ saveCheckoutCustomer(domainId, {
59
+ id: user.id,
60
+ name: customerJson.name,
61
+ emailAddress: customerJson.emailAddresses?.[0]?.emailAddress,
62
+ phoneNumbers: customerJson.phoneNumbers,
63
+ });
53
64
  setCartClient(cartJson.client);
54
65
  setCart(cartJson);
55
66
  } catch (e: any) {
@@ -61,9 +72,14 @@ export function FormCustomerNew() {
61
72
 
62
73
  const { control, handleSubmit, watch } = useForm({
63
74
  defaultValues: {
64
- emailAddresses: [{ emailAddress: '' }],
65
- name: '',
66
- phoneNumbers: [{ code: 'AU', number: '' }],
75
+ emailAddresses: [{
76
+ emailAddress: savedCustomer?.emailAddress || '',
77
+ }],
78
+ name: savedCustomer?.name || '',
79
+ phoneNumbers: [{
80
+ code: savedCustomer?.phoneNumbers?.[0]?.code || 'AU',
81
+ number: savedCustomer?.phoneNumbers?.[0]?.number || '',
82
+ }],
67
83
  registeredAsGuest: false,
68
84
  },
69
85
  });
@@ -10,6 +10,10 @@ import {
10
10
  import { makeCart } from '../utilities/cart';
11
11
  import { getCartCookieToken } from '../utilities/cookie';
12
12
  import { cartEmbed } from '../utilities/helpers';
13
+ import {
14
+ getSavedCheckoutCustomer,
15
+ saveCheckoutCustomer,
16
+ } from '../utilities/local_storage';
13
17
 
14
18
  const emailValidation = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]/i;
15
19
 
@@ -27,8 +31,11 @@ function FormReturningCustomer() {
27
31
  } = useCartContext();
28
32
  const [loading, setLoading] = React.useState(false);
29
33
  const [error, setError] = React.useState(({} as any));
34
+ const savedCustomer = getSavedCheckoutCustomer(domainId);
30
35
 
31
- const hookForm = useForm({ defaultValues: { emailAddress: '' } });
36
+ const hookForm = useForm({
37
+ defaultValues: { emailAddress: savedCustomer?.emailAddress || '' },
38
+ });
32
39
  const {
33
40
  handleSubmit,
34
41
  formState: { errors },
@@ -46,15 +53,10 @@ function FormReturningCustomer() {
46
53
  if (!userId) {
47
54
  throw new Error('Unable to find returning customer.');
48
55
  }
49
- const userJson = {
50
- id: userId,
51
- emailAddresses: [{emailAddress}],
52
- name: user.name || 'Hidden for privacy'
53
- };
54
56
 
55
- // patch cart with returning user
57
+ // Attach by id only — extra user fields trigger a forbidden user PATCH.
56
58
  const cartToken = await getCartCookieToken((domainId as number));
57
- const clientEnt = makeUser(userJson, true);
59
+ const clientEnt = makeUser({id: userId}, true);
58
60
  const cartEnt = makeCart({...cart}, false, cartToken);
59
61
  cartEnt.client = clientEnt;
60
62
  const _cart = await cartEnt.save({embed: cartEmbed});
@@ -62,6 +64,11 @@ function FormReturningCustomer() {
62
64
  if (!cartJson?.client?.id) {
63
65
  throw new Error('Unable to attach client to cart.');
64
66
  }
67
+ saveCheckoutCustomer(domainId, {
68
+ id: userId,
69
+ name: user.name || savedCustomer?.name,
70
+ emailAddress,
71
+ });
65
72
  setCartClient(cartJson.client);
66
73
  setCart(cartJson);
67
74
  } catch(e: any) {
@@ -11,6 +11,11 @@ import { makeAddress } from "../utilities/address";
11
11
  import { makeCart } from "../utilities/cart";
12
12
  import { cartEmbed } from "../utilities/helpers";
13
13
  import { tabIdCheckout } from "../utilities/tabs";
14
+ import {
15
+ addressHasContent,
16
+ getSavedCheckoutAddress,
17
+ saveCheckoutAddress,
18
+ } from "../utilities/local_storage";
14
19
 
15
20
  interface PropsAddress {
16
21
  defaultAddress?: any;
@@ -110,7 +115,12 @@ export function ActiveFormShipmentAddressAndNotes() {
110
115
  setLoadingTotals,
111
116
  setActiveTabAndEditDisabled,
112
117
  } = useCartContext();
113
- const { receiverAddress = {}, receiverNotes } = cart;
118
+ const saved = getSavedCheckoutAddress(domainId);
119
+ const cartAddress = cart?.receiverAddress;
120
+ const receiverAddress = addressHasContent(cartAddress)
121
+ ? cartAddress
122
+ : saved.address || {};
123
+ const receiverNotes = cart?.receiverNotes || saved.receiverNotes || "";
114
124
  const [addressFieldsOpen, setAddressFieldsOpen] = useState(false);
115
125
 
116
126
  async function saveCartShipmentAddressAndGoToNextTab(values: any) {
@@ -123,6 +133,7 @@ export function ActiveFormShipmentAddressAndNotes() {
123
133
  cartEnt.receiverNotes = receiverNotes;
124
134
  try {
125
135
  await cartEnt.save({ embed: cartEmbed });
136
+ saveCheckoutAddress(domainId, address, receiverNotes);
126
137
  setCart(cartEnt.toJson()); // Update cart context
127
138
  setActiveTabAndEditDisabled({
128
139
  tabId: tabIdCheckout,
@@ -6,6 +6,7 @@ export * from './currency';
6
6
  export * from './helpers';
7
7
  export * from './invoice';
8
8
  export * from './job';
9
+ export * from './local_storage';
9
10
  export * from './product';
10
11
  export * from './shipment';
11
12
  export * from './tabs';
@@ -0,0 +1,130 @@
1
+ import { isBrowser } from 'browser-or-node';
2
+
3
+ export interface SavedCheckoutCustomer {
4
+ id?: number;
5
+ name?: string;
6
+ emailAddress?: string;
7
+ phoneNumbers?: Array<{ code?: string; number?: string }>;
8
+ }
9
+
10
+ export interface SavedCheckoutAddress {
11
+ city?: string;
12
+ country?: string;
13
+ lineOne?: string;
14
+ lineTwo?: string;
15
+ postcode?: string;
16
+ state?: string;
17
+ }
18
+
19
+ export interface SavedCheckoutDraft {
20
+ customer?: SavedCheckoutCustomer;
21
+ address?: SavedCheckoutAddress;
22
+ receiverNotes?: string;
23
+ }
24
+
25
+ function storageKey(domainId: number | string) {
26
+ return `merchi-cart-checkout-${domainId}`;
27
+ }
28
+
29
+ function readDraft(domainId?: number | string | null): SavedCheckoutDraft {
30
+ if (!isBrowser || domainId === undefined || domainId === null) {
31
+ return {};
32
+ }
33
+ try {
34
+ const raw = localStorage.getItem(storageKey(domainId));
35
+ if (!raw) return {};
36
+ const parsed = JSON.parse(raw);
37
+ return parsed && typeof parsed === 'object' ? parsed : {};
38
+ } catch {
39
+ return {};
40
+ }
41
+ }
42
+
43
+ function writeDraft(domainId: number | string | undefined | null, draft: SavedCheckoutDraft) {
44
+ if (!isBrowser || domainId === undefined || domainId === null) {
45
+ return;
46
+ }
47
+ try {
48
+ localStorage.setItem(storageKey(domainId), JSON.stringify(draft));
49
+ } catch {
50
+ // Ignore quota / private-mode failures.
51
+ }
52
+ }
53
+
54
+ export function getSavedCheckoutDraft(domainId?: number | string | null): SavedCheckoutDraft {
55
+ return readDraft(domainId);
56
+ }
57
+
58
+ export function getSavedCheckoutCustomer(
59
+ domainId?: number | string | null
60
+ ): SavedCheckoutCustomer | undefined {
61
+ return readDraft(domainId).customer;
62
+ }
63
+
64
+ export function getSavedCheckoutAddress(
65
+ domainId?: number | string | null
66
+ ): { address?: SavedCheckoutAddress; receiverNotes?: string } {
67
+ const draft = readDraft(domainId);
68
+ return {
69
+ address: draft.address,
70
+ receiverNotes: draft.receiverNotes,
71
+ };
72
+ }
73
+
74
+ export function saveCheckoutCustomer(
75
+ domainId: number | string | undefined | null,
76
+ customer: SavedCheckoutCustomer
77
+ ) {
78
+ const draft = readDraft(domainId);
79
+ const nextCustomer: SavedCheckoutCustomer = {
80
+ ...draft.customer,
81
+ ...customer,
82
+ };
83
+ // Drop empty phone entries.
84
+ if (nextCustomer.phoneNumbers) {
85
+ nextCustomer.phoneNumbers = nextCustomer.phoneNumbers.filter(
86
+ (phone) => phone && (phone.number || phone.code)
87
+ );
88
+ if (!nextCustomer.phoneNumbers.length) {
89
+ delete nextCustomer.phoneNumbers;
90
+ }
91
+ }
92
+ writeDraft(domainId, { ...draft, customer: nextCustomer });
93
+ }
94
+
95
+ export function saveCheckoutAddress(
96
+ domainId: number | string | undefined | null,
97
+ address: SavedCheckoutAddress | null | undefined,
98
+ receiverNotes?: string
99
+ ) {
100
+ if (!address) return;
101
+ const draft = readDraft(domainId);
102
+ const nextAddress: SavedCheckoutAddress = {
103
+ city: address.city || '',
104
+ country: address.country || '',
105
+ lineOne: address.lineOne || '',
106
+ lineTwo: address.lineTwo || '',
107
+ postcode: address.postcode || '',
108
+ state: address.state || '',
109
+ };
110
+ const next: SavedCheckoutDraft = {
111
+ ...draft,
112
+ address: nextAddress,
113
+ };
114
+ if (receiverNotes !== undefined) {
115
+ next.receiverNotes = receiverNotes;
116
+ }
117
+ writeDraft(domainId, next);
118
+ }
119
+
120
+ export function addressHasContent(address?: SavedCheckoutAddress | null) {
121
+ if (!address) return false;
122
+ return Boolean(
123
+ address.lineOne ||
124
+ address.lineTwo ||
125
+ address.city ||
126
+ address.state ||
127
+ address.postcode ||
128
+ address.country
129
+ );
130
+ }