@ordergroove/offers 2.34.8-alpha-PR-793-2.0 → 2.35.0

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.
@@ -4,14 +4,18 @@ describe('Shopify Utils', () => {
4
4
  describe('Money', () => {
5
5
  it('Should return formatted greater than $1 money price', () => {
6
6
  const price = 1000;
7
- const formattedPrice = money(price);
7
+ const formattedPrice = money(price, 'USD');
8
8
  expect(formattedPrice).toBe('$10.00');
9
9
  });
10
10
 
11
11
  it('Should return formatted lower than $1 money price', () => {
12
12
  const price = 50;
13
- const formattedPrice = money(price);
14
- expect(formattedPrice).toBe('$.50');
13
+ const formattedPrice = money(price, 'USD');
14
+ expect(formattedPrice).toBe('$0.50');
15
+ });
16
+
17
+ it('Should format JPY values', () => {
18
+ expect(money(310000, 'JPY')).toBe('¥3,100');
15
19
  });
16
20
  });
17
21
 
@@ -20,25 +20,25 @@ export const getAllocationFrequency = (allocation: ShopifySellingPlanAllocations
20
20
  return (allocation.selling_plan_id || (allocation.selling_plan?.id ?? '')).toString();
21
21
  };
22
22
 
23
- export const getAllocationRegularPrice = (allocation: ShopifySellingPlanAllocationsEntity) => {
24
- return money(allocation.compare_at_price);
23
+ export const getAllocationRegularPrice = (allocation: ShopifySellingPlanAllocationsEntity, currency: string) => {
24
+ return money(allocation.compare_at_price, currency);
25
25
  };
26
26
 
27
- export const getAllocationSubscriptionPrice = (allocation: ShopifySellingPlanAllocationsEntity) => {
27
+ export const getAllocationSubscriptionPrice = (allocation: ShopifySellingPlanAllocationsEntity, currency: string) => {
28
28
  if (isPrepaidAllocation(allocation)) {
29
29
  const prepaidShipmentsPerBilling = getPrepaidShipmentsNumberFromOptions(allocation.selling_plan?.options);
30
30
  const pricePerShipment = Math.round(allocation.price / prepaidShipmentsPerBilling);
31
- return money(pricePerShipment);
31
+ return money(pricePerShipment, currency);
32
32
  }
33
33
 
34
- return money(allocation.price);
34
+ return money(allocation.price, currency);
35
35
  };
36
36
 
37
37
  const getPrepaidPercentage = (allocation: ShopifySellingPlanAllocationsEntity, pricePerShipment: number) => {
38
38
  return Math.round(((allocation.compare_at_price - pricePerShipment) * 100) / allocation.compare_at_price);
39
39
  };
40
40
 
41
- export const getAllocationDiscountRate = (allocation: ShopifySellingPlanAllocationsEntity) => {
41
+ export const getAllocationDiscountRate = (allocation: ShopifySellingPlanAllocationsEntity, currency: string) => {
42
42
  if (isPrepaidAllocation(allocation)) {
43
43
  const prepaidShipmentsPerBilling = getPrepaidShipmentsNumberFromOptions(allocation.selling_plan?.options);
44
44
  const pricePerShipment = allocation.price / prepaidShipmentsPerBilling;
@@ -52,9 +52,9 @@ export const getAllocationDiscountRate = (allocation: ShopifySellingPlanAllocati
52
52
  if (allocation.price_adjustments[0]?.value_type === 'percentage') {
53
53
  formatted_discount = percentage(allocation.price_adjustments[0].value);
54
54
  } else if (allocation.price_adjustments[0]?.value) {
55
- formatted_discount = money(allocation.price_adjustments[0].value);
55
+ formatted_discount = money(allocation.price_adjustments[0].value, currency);
56
56
  } else if (allocation.compare_at_price) {
57
- formatted_discount = money(allocation.compare_at_price - allocation.price);
57
+ formatted_discount = money(allocation.compare_at_price - allocation.price, currency);
58
58
  }
59
59
 
60
60
  return formatted_discount;
@@ -70,7 +70,8 @@ export const getAllocationNumberOfShipments = (allocation: ShopifySellingPlanAll
70
70
  export const addPrepaidPriceAndSavings = (
71
71
  allocation: ShopifySellingPlanAllocationsEntity,
72
72
  productPlan: ProductPlanEntity,
73
- payAsYouGoPlan: ShopifySellingPlansEntity
73
+ payAsYouGoPlan: ShopifySellingPlansEntity,
74
+ currency: string
74
75
  ) => {
75
76
  const prepaidShipmentsPerBilling = getPrepaidShipmentsNumberFromOptions(allocation.selling_plan?.options);
76
77
  const pricePerShipment = allocation.price / prepaidShipmentsPerBilling;
@@ -80,9 +81,9 @@ export const addPrepaidPriceAndSavings = (
80
81
  const payAsYouGoPercentage =
81
82
  payAsYouGoAdjustment && payAsYouGoAdjustment.value_type === 'percentage' ? payAsYouGoAdjustment.value : null;
82
83
 
83
- productPlan['regularPrepaidPrice'] = money(allocation.price);
84
- productPlan['prepaidSavingsPerShipment'] = money(Math.round(prepaidSaving));
85
- productPlan['prepaidSavingsTotal'] = money(Math.round(prepaidSaving * prepaidShipmentsPerBilling));
84
+ productPlan['regularPrepaidPrice'] = money(allocation.price, currency);
85
+ productPlan['prepaidSavingsPerShipment'] = money(Math.round(prepaidSaving), currency);
86
+ productPlan['prepaidSavingsTotal'] = money(Math.round(prepaidSaving * prepaidShipmentsPerBilling), currency);
86
87
 
87
88
  if (payAsYouGoPercentage && prepaidPercentageSavings) {
88
89
  productPlan['prepaidExtraSavingsPercentage'] = percentage(prepaidPercentageSavings - payAsYouGoPercentage);
@@ -95,7 +96,8 @@ export const DEFAULT_PAY_AS_YOU_GO_GROUP_NAME = 'Subscribe and Save';
95
96
 
96
97
  export const mapSellingPlanToDiscount = (
97
98
  allocation: ShopifySellingPlanAllocationsEntity,
98
- sellingPlans: ShopifySellingPlansEntity[] = []
99
+ sellingPlans: ShopifySellingPlansEntity[],
100
+ currency: string
99
101
  ) => {
100
102
  if (!allocation.selling_plan) {
101
103
  allocation.selling_plan = sellingPlans.find(plan => plan.id === allocation.selling_plan_id);
@@ -103,9 +105,9 @@ export const mapSellingPlanToDiscount = (
103
105
 
104
106
  const productPlan: ProductPlanEntity = {
105
107
  frequency: getAllocationFrequency(allocation),
106
- regularPrice: getAllocationRegularPrice(allocation),
107
- subscriptionPrice: getAllocationSubscriptionPrice(allocation),
108
- discountRate: getAllocationDiscountRate(allocation),
108
+ regularPrice: getAllocationRegularPrice(allocation, currency),
109
+ subscriptionPrice: getAllocationSubscriptionPrice(allocation, currency),
110
+ discountRate: getAllocationDiscountRate(allocation, currency),
109
111
  prepaidShipments: getAllocationNumberOfShipments(allocation)
110
112
  };
111
113
 
@@ -113,19 +115,21 @@ export const mapSellingPlanToDiscount = (
113
115
  const payAsYouGoPlan = sellingPlans.find(
114
116
  plan => plan.group_name === DEFAULT_PAY_AS_YOU_GO_GROUP_NAME && plan.options.length === 1
115
117
  );
116
- return addPrepaidPriceAndSavings(allocation, productPlan, payAsYouGoPlan);
118
+ return addPrepaidPriceAndSavings(allocation, productPlan, payAsYouGoPlan, currency);
117
119
  }
118
120
 
119
121
  return productPlan;
120
122
  };
121
123
 
122
- export const sellingPlanAllocationsReducer = (acc, cur, sellingPlans = []) => [
123
- ...acc,
124
- mapSellingPlanToDiscount(cur, sellingPlans)
125
- ];
124
+ export const sellingPlanAllocationsReducer = (
125
+ acc: ProductPlanEntity[],
126
+ cur: ShopifySellingPlanAllocationsEntity,
127
+ sellingPlans: ShopifySellingPlansEntity[] = [],
128
+ currency: string
129
+ ) => [...acc, mapSellingPlanToDiscount(cur, sellingPlans, currency)];
126
130
 
127
131
  export const getSellingPlans = (product: ShopifyProductEntity) =>
128
- product.selling_plan_groups.reduce(
132
+ product.selling_plan_groups.reduce<ShopifySellingPlansEntity[]>(
129
133
  (allGroups, group) => [
130
134
  ...allGroups,
131
135
  ...group.selling_plans.map(selling_plan => ({ ...selling_plan, group_name: group.name }))
@@ -45,7 +45,15 @@ declare global {
45
45
  previewMode: boolean;
46
46
  };
47
47
  ogShopifyConfig: OgShopifyConfig;
48
- Shopify: { routes?: { root: string } };
48
+ Shopify?: {
49
+ routes?: {
50
+ root: string;
51
+ };
52
+ currency?: {
53
+ active: string;
54
+ rate: string;
55
+ };
56
+ };
49
57
  }
50
58
  }
51
59
 
@@ -17,12 +17,22 @@ import {
17
17
  import { makeSubscribedSelector } from '../core/selectors';
18
18
  import { getOrCreateHidden, safeProductId } from '../core/utils';
19
19
  import { getTrackingKey } from './shopifyTrackingMiddleware';
20
+ import { ShopifyCart, ShopifyProductEntity } from './types/shopify';
20
21
 
21
22
  const SHOPIFY_ROOT = window.Shopify?.routes?.root || '/';
22
23
  const CART_PAGE_URL = '/cart';
23
24
  const CART_JS_URL = `${SHOPIFY_ROOT}cart.js`;
25
+ const CART_CHANGE_URL = `${SHOPIFY_ROOT}cart/change.js`;
24
26
  const PRODUCTS_URL = `${SHOPIFY_ROOT}products/`;
25
27
 
28
+ type SetupProductPayload = {
29
+ product: ShopifyProductEntity;
30
+ offer: any;
31
+ currency: string;
32
+ };
33
+
34
+ type SetupCartPayload = ShopifyCart;
35
+
26
36
  /**
27
37
  * List of section DOM elements to update via section-rendering api https://shopify.dev/api/section-rendering
28
38
  */
@@ -38,12 +48,22 @@ const makeSyncProductId = offer =>
38
48
  }
39
49
  });
40
50
 
51
+ async function getCurrency() {
52
+ const windowCurrency = window.Shopify?.currency?.active;
53
+ if (windowCurrency) {
54
+ return windowCurrency;
55
+ }
56
+ const cart = await getCart();
57
+ return cart.currency;
58
+ }
59
+
41
60
  async function setupPdp(store, offer) {
42
61
  const handle = guessProductHandle(offer);
43
62
  if (handle) {
44
63
  try {
45
- const product = await getProduct(handle);
46
- store.dispatch({ type: SETUP_PRODUCT, payload: { product, offer } });
64
+ const [product, currency] = await Promise.all([getProduct(handle), getCurrency()]);
65
+ const payload: SetupProductPayload = { product, offer, currency: currency };
66
+ store.dispatch({ type: SETUP_PRODUCT, payload });
47
67
  } catch (err) {
48
68
  console.warn('OG: Unable to fetch product details for PDP', err);
49
69
  }
@@ -79,7 +99,9 @@ async function setupPdp(store, offer) {
79
99
  }
80
100
  }
81
101
 
82
- const getCart = async () => await (await fetch(CART_JS_URL)).json();
102
+ async function getCart(): Promise<ShopifyCart> {
103
+ return (await fetch(CART_JS_URL)).json();
104
+ }
83
105
 
84
106
  /**
85
107
  * Attemps to guess the product handle o
@@ -120,12 +142,15 @@ export function guessProductHandle(offer): string {
120
142
  );
121
143
  }
122
144
 
123
- const getProduct = memoize(async handle => (await fetch(`${PRODUCTS_URL}${handle}.js`)).json());
145
+ const getProduct = memoize(async function(handle: string): Promise<ShopifyProductEntity> {
146
+ return (await fetch(`${PRODUCTS_URL}${handle}.js`)).json();
147
+ });
124
148
 
125
149
  async function setupCart(store, offer) {
126
150
  const cart = await getCart();
127
151
  const { items } = cart;
128
- store.dispatch({ type: SETUP_CART, payload: cart });
152
+ const cartPayload: SetupCartPayload = cart;
153
+ store.dispatch({ type: SETUP_CART, payload: cartPayload });
129
154
 
130
155
  // some minicart templates does not contains line.key but contains line which corresponds to
131
156
  // the index on the cart items (Vedge)
@@ -136,7 +161,10 @@ async function setupCart(store, offer) {
136
161
  }
137
162
 
138
163
  const products = await Promise.all(Array.from(new Set(items.map(({ handle }) => handle))).map(getProduct));
139
- products.forEach(product => store.dispatch({ type: SETUP_PRODUCT, payload: { product, offer } }));
164
+ products.forEach(product => {
165
+ const payload: SetupProductPayload = { product, offer, currency: cart.currency };
166
+ store.dispatch({ type: SETUP_PRODUCT, payload });
167
+ });
140
168
  }
141
169
 
142
170
  /**
@@ -168,7 +196,7 @@ export async function synchronizeCartOptin(action: any, store: any) {
168
196
  const qty = item.quantity;
169
197
  const productId = safeProductId(key);
170
198
 
171
- const res = await fetch('/cart/change.js', {
199
+ const res = await fetch(CART_CHANGE_URL, {
172
200
  method: 'POST',
173
201
  credentials: 'same-origin',
174
202
  headers: { 'Content-Type': 'application/json' },
@@ -184,7 +212,7 @@ export async function synchronizeCartOptin(action: any, store: any) {
184
212
 
185
213
  if (res.status !== 200) throw new Error('Cart not updated');
186
214
 
187
- const newCart = await res.json();
215
+ const newCart: ShopifyCart = await res.json();
188
216
 
189
217
  // If both carts have same length we can update the item.key
190
218
  // to the original offer element, at least provide
@@ -213,7 +241,8 @@ export async function synchronizeCartOptin(action: any, store: any) {
213
241
  }
214
242
 
215
243
  // dispatch SETUP_CART so offer does not flip the state
216
- store.dispatch({ type: SETUP_CART, payload: newCart });
244
+ const newCartPayload: SetupCartPayload = newCart;
245
+ store.dispatch({ type: SETUP_CART, payload: newCartPayload });
217
246
 
218
247
  // Use a custom event to hook custom cart updates.
219
248
  const cartUpdateEvent = new CustomEvent(CART_UPDATED_EVENT, { bubbles: true, cancelable: true });
@@ -440,7 +440,7 @@ export const productOffer = (state = {}, _action) => state;
440
440
  export const productPlans = (state = {}, action) => {
441
441
  if (constants.SETUP_PRODUCT === action.type) {
442
442
  const {
443
- payload: { product }
443
+ payload: { product, currency }
444
444
  } = action;
445
445
 
446
446
  const sellingPlans = getSellingPlans(product);
@@ -451,7 +451,7 @@ export const productPlans = (state = {}, action) => {
451
451
  (acc, cur) => ({
452
452
  ...acc,
453
453
  [cur.id]: cur.selling_plan_allocations?.reduce(
454
- (accumulator, current) => sellingPlanAllocationsReducer(accumulator, current, sellingPlans),
454
+ (accumulator, current) => sellingPlanAllocationsReducer(accumulator, current, sellingPlans, currency),
455
455
  []
456
456
  )
457
457
  }),
@@ -467,7 +467,7 @@ export const productPlans = (state = {}, action) => {
467
467
  cur.selling_plan_allocation
468
468
  ? {
469
469
  ...acc,
470
- [cur.key]: sellingPlanAllocationsReducer([], cur.selling_plan_allocation)
470
+ [cur.key]: sellingPlanAllocationsReducer([], cur.selling_plan_allocation, [], cart.currency)
471
471
  }
472
472
  : acc,
473
473
  state
@@ -96,3 +96,136 @@ export interface ShopifyProductEntity {
96
96
  requires_selling_plan: boolean;
97
97
  selling_plan_groups?: ShopifySellingPlanGroupsEntity[] | null;
98
98
  }
99
+
100
+ export interface ShopifyCart {
101
+ token: string;
102
+ note: string;
103
+ attributes: Record<string, string>;
104
+ original_total_price: number;
105
+ total_price: number;
106
+ total_discount: number;
107
+ total_weight: number;
108
+ item_count: number;
109
+ items: CartItem[];
110
+ requires_shipping: boolean;
111
+ currency: string;
112
+ items_subtotal_price: number;
113
+ cart_level_discount_applications: CartLevelDiscountApplication[];
114
+ sections?: Record<string, string>;
115
+ }
116
+
117
+ interface CartItem {
118
+ id: number;
119
+ properties: Record<string, string>;
120
+ quantity: number;
121
+ variant_id: number;
122
+ key: string;
123
+ title: string;
124
+ price: number;
125
+ original_price: number;
126
+ discounted_price: number;
127
+ line_price: number;
128
+ original_line_price: number;
129
+ total_discount: number;
130
+ discounts: Discount[];
131
+ sku: string;
132
+ grams: number;
133
+ vendor: string;
134
+ taxable: boolean;
135
+ product_id: number;
136
+ product_has_only_default_variant: boolean;
137
+ gift_card: boolean;
138
+ final_price: number;
139
+ final_line_price: number;
140
+ url: string;
141
+ featured_image: FeaturedImage;
142
+ image: string;
143
+ handle: string;
144
+ requires_shipping: boolean;
145
+ product_type: string;
146
+ product_title: string;
147
+ product_description: string;
148
+ variant_title?: string;
149
+ variant_options: string[];
150
+ options_with_values: OptionsWithValue[];
151
+ line_level_discount_allocations: LineLevelDiscountAllocation[];
152
+ line_level_total_discount: number;
153
+ quantity_rule: QuantityRule;
154
+ has_components: boolean;
155
+ selling_plan_allocation?: SellingPlanAllocation;
156
+ unit_price?: number;
157
+ unit_price_measurement?: UnitPriceMeasurement;
158
+ }
159
+
160
+ interface Discount {
161
+ amount: number;
162
+ title: string;
163
+ }
164
+
165
+ interface FeaturedImage {
166
+ aspect_ratio: number;
167
+ alt: string;
168
+ height: number;
169
+ url: string;
170
+ width: number;
171
+ }
172
+
173
+ interface OptionsWithValue {
174
+ name: string;
175
+ value: string;
176
+ }
177
+
178
+ interface LineLevelDiscountAllocation {
179
+ amount: number;
180
+ discount_application: DiscountApplication;
181
+ }
182
+
183
+ interface DiscountApplication {
184
+ type: string;
185
+ key: string;
186
+ title: string;
187
+ description: string;
188
+ value: string;
189
+ created_at: string;
190
+ value_type: string;
191
+ allocation_method: string;
192
+ target_selection: string;
193
+ target_type: string;
194
+ total_allocated_amount: number;
195
+ }
196
+
197
+ interface QuantityRule {
198
+ min: number;
199
+ max?: number;
200
+ increment: number;
201
+ }
202
+
203
+ interface SellingPlanAllocation {
204
+ price_adjustments: ShopifyPriceAdjustmentsEntity[];
205
+ price: number;
206
+ compare_at_price: number;
207
+ per_delivery_price: number;
208
+ selling_plan: ShopifySellingPlansEntity;
209
+ }
210
+
211
+ interface UnitPriceMeasurement {
212
+ measured_type: string;
213
+ quantity_value: string;
214
+ quantity_unit: string;
215
+ reference_value: number;
216
+ reference_unit: string;
217
+ }
218
+
219
+ interface CartLevelDiscountApplication {
220
+ type: string;
221
+ key: string;
222
+ title: string;
223
+ description: string;
224
+ value: string;
225
+ created_at: string;
226
+ value_type: string;
227
+ allocation_method: string;
228
+ target_selection: string;
229
+ target_type: string;
230
+ total_allocated_amount: number;
231
+ }
@@ -1,3 +1,9 @@
1
- export const money = val => (val === null ? '' : `$${val.toString().replace(/(\d\d)$/, '.$1')}`);
1
+ export const money = (val: number, currency: string) =>
2
+ val === null
3
+ ? ''
4
+ : new Intl.NumberFormat(navigator.language, {
5
+ style: 'currency',
6
+ currency
7
+ }).format(val / 100);
2
8
 
3
9
  export const percentage = val => `${val}%`;