@rebilly/instruments 3.18.1-beta.0 → 3.20.0-beta.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rebilly/instruments",
3
- "version": "3.18.1-beta.0",
3
+ "version": "3.20.0-beta.0",
4
4
  "author": "Rebilly",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -2273,6 +2273,25 @@
2273
2273
  }
2274
2274
  ]
2275
2275
  },
2276
+ {
2277
+ "apiName": "SafetyPay",
2278
+ "name": "SafetyPay",
2279
+ "landscapeLogo": null,
2280
+ "portraitLogo": null,
2281
+ "summary": "SafetyPay operates the largest network of banks and cash collection points in Latin America, the result of 10+ years effort, with presence in 16 countries consolidated with 380 bank partners and 180,000 collection points.\n",
2282
+ "description": "SafetyPay operates the largest network of banks and cash collection points in Latin America, the result of 10+ years effort, with presence in 16 countries consolidated with 380 bank partners and 180,000 collection points.\n",
2283
+ "countries": {
2284
+ "mode": "unknown",
2285
+ "values": []
2286
+ },
2287
+ "storefrontEnabled": true,
2288
+ "_links": [
2289
+ {
2290
+ "rel": "self",
2291
+ "href": "https://api.rebilly.com/payment-methods/SafetyPay"
2292
+ }
2293
+ ]
2294
+ },
2276
2295
  {
2277
2296
  "apiName": "Siirto",
2278
2297
  "name": "Siirto",
@@ -1,5 +1,6 @@
1
1
  import { collectData } from '@rebilly/risk-data-collector';
2
2
  import { fetchProductsFromPlans } from '../../storefront/fetch-products-from-plans';
3
+ import { fetchPlansFromAddonsBumpOffers } from '../../storefront/fetch-plans-from-addons-bumpOffers';
3
4
  import { fetchReadyToPay } from '../../storefront/ready-to-pay';
4
5
  import { fetchSummary } from '../../storefront/summary';
5
6
  import { fetchInvoiceAndProducts as FetchInvoiceAndProducts } from '../../storefront/invoices';
@@ -21,6 +22,7 @@ export class DataInstance {
21
22
 
22
23
  this.money = state.options?.money || null;
23
24
  this.couponIds = [];
25
+ this.addons = [];
24
26
  }
25
27
 
26
28
  get amountAndCurrency() {
@@ -174,15 +176,22 @@ export async function fetchData({
174
176
  productsPromise = fetchProductsFromPlans({ state });
175
177
  }
176
178
 
179
+ let plansPromise = new Promise((resolve) => resolve([]));
180
+ if (state.options?.addons || state.options?.bumpOffers) {
181
+ plansPromise = fetchPlansFromAddonsBumpOffers({ state });
182
+ }
183
+
177
184
  const [
178
185
  readyToPay,
179
186
  previewPurchase,
180
187
  products,
188
+ plans,
181
189
  availableInstruments
182
190
  ] = await Promise.all([
183
191
  readyToPayPromise,
184
192
  previewPurchasePromise,
185
193
  productsPromise,
194
+ plansPromise,
186
195
  availableInstrumentsPromise
187
196
  ]);
188
197
 
@@ -191,6 +200,7 @@ export async function fetchData({
191
200
  readyToPay,
192
201
  previewPurchase,
193
202
  products,
203
+ plans,
194
204
  availableInstruments
195
205
  });
196
206
  } catch(error) {
@@ -7,6 +7,8 @@ export const defaults = {
7
7
  theme: {
8
8
  labels: 'stacked'
9
9
  },
10
+ addons: [],
11
+ bumpOffers: [],
10
12
  paymentInstruments: {
11
13
  address: {
12
14
  name: 'default',
@@ -136,6 +138,7 @@ export default ({
136
138
  // Add optional key's
137
139
  [
138
140
  'items',
141
+ 'bumpOffers',
139
142
  'money',
140
143
  'invoiceId',
141
144
  'transactionId',
@@ -147,5 +150,12 @@ export default ({
147
150
  }
148
151
  });
149
152
 
153
+ // only add "addons" if items are available
154
+ if (combinedOptions.items) {
155
+ if (options.addons) {
156
+ combinedOptions.addons = options.addons;
157
+ }
158
+ }
159
+
150
160
  return combinedOptions;
151
161
  }
@@ -74,6 +74,14 @@ export async function makePurchase({ state, payload }) {
74
74
  data.couponIds = state.data.couponIds
75
75
  }
76
76
 
77
+ if (state.options.addons && state.data.addons && Array.isArray(state.data.addons)) {
78
+ state.options.addons.forEach(addon => {
79
+ if (state.data.addons.includes(addon.planId)) {
80
+ data.items.push(addon);
81
+ }
82
+ })
83
+ }
84
+
77
85
  const { fields } = await postPurchase({
78
86
  state,
79
87
  data
@@ -81,30 +89,40 @@ export async function makePurchase({ state, payload }) {
81
89
  return fields;
82
90
  }
83
91
 
84
- export function handleApprovalUrl({state, fields}) {
85
- const { paymentMethodsUrl } = state.options._computed;
92
+ export function handleApprovalUrl({state, fields, payload}) {
93
+ if (payload.redirectUrl || !fields.transaction?.approvalUrl) {
94
+ const { paymentMethodsUrl } = state.options._computed;
86
95
 
87
- const model = {};
88
- if (state.data.isPayment) {
89
- model.payment = fields;
90
- } else {
91
- model.purchase = fields;
92
- }
93
-
94
- state.data = new DataInstance({state, ...fields});
95
-
96
- mountModal({
97
- state,
98
- name: 'rebilly-instruments-approval-url',
99
- url: `${paymentMethodsUrl}/approval-url`,
100
- model,
101
- close: (updatedPurchase) => {
102
- Events.purchaseCompleted.dispatch(updatedPurchase);
96
+ const model = {};
97
+ if (state.data.isPayment) {
98
+ model.payment = fields;
99
+ } else {
100
+ model.purchase = fields;
103
101
  }
104
- });
102
+
103
+ state.data = new DataInstance({state, ...fields});
104
+
105
+ mountModal({
106
+ state,
107
+ name: 'rebilly-instruments-approval-url',
108
+ url: `${paymentMethodsUrl}/approval-url`,
109
+ model,
110
+ close: (updatedPurchase) => {
111
+ Events.purchaseCompleted.dispatch(updatedPurchase);
112
+ }
113
+ });
114
+ } else if (fields.transaction?.approvalUrl) {
115
+ window.location = fields.transaction?.approvalUrl
116
+ }
105
117
  }
106
118
 
107
119
  export async function purchase({ state, payload }) {
120
+ Object.keys(payload).forEach(key => {
121
+ if (payload[key] === null) {
122
+ delete payload[key];
123
+ }
124
+ });
125
+
108
126
  try {
109
127
  let fields;
110
128
  if (state.data.isPayment) {
@@ -114,7 +132,7 @@ export async function purchase({ state, payload }) {
114
132
  }
115
133
 
116
134
  if (fields.transaction?.approvalUrl && fields.transaction?.result === 'unknown') {
117
- handleApprovalUrl({state, fields});
135
+ handleApprovalUrl({state, fields, payload});
118
136
  } else {
119
137
  Events.purchaseCompleted.dispatch(fields);
120
138
  }
@@ -0,0 +1,27 @@
1
+ import PlanModel from './models/plan-model';
2
+ import { Endpoint } from './index';
3
+
4
+ export async function fetchPlansFromAddonsBumpOffers({ state = {} }) {
5
+ return Endpoint({state}, async () => {
6
+ let ids = [];
7
+
8
+ if (state.options?.addons) {
9
+ ids = ids.concat(state.options.addons
10
+ .map((item) => item.planId));
11
+ }
12
+
13
+ if (state.options?.bumpOffers) {
14
+ ids = ids.concat(state.options.bumpOffers
15
+ .map((item) => item.planId));
16
+ }
17
+
18
+ if (ids.length > 0) {
19
+ const { items: planItems } = await state.storefront.plans.getAll({
20
+ filter: `id:${ids.join(',')}`
21
+ });
22
+ return planItems.map(({fields}) => new PlanModel(fields).toPayload());
23
+ }
24
+
25
+ return [];
26
+ });
27
+ }
@@ -15,9 +15,20 @@ export async function fetchProductsFromPlans({ state = {} }) {
15
15
  };
16
16
 
17
17
  if (lineItems.length) {
18
- filterByPlanId.filter = `id:${lineItems
19
- .map((item) => item.planId)
20
- .join(',')}`;
18
+ let ids = lineItems
19
+ .map((item) => item.planId);
20
+
21
+ if (state.options?.addons) {
22
+ ids = ids.concat(state.options.addons
23
+ .map((item) => item.planId));
24
+ }
25
+
26
+ if (state.options?.bumpOffers) {
27
+ ids = ids.concat(state.options.bumpOffers
28
+ .map((item) => item.planId));
29
+ }
30
+
31
+ filterByPlanId.filter = `id:${[...new Set(ids)].join(',')}`;
21
32
  }
22
33
 
23
34
  // Only fetch plans if we have specific plans to fetch
@@ -28,7 +39,15 @@ export async function fetchProductsFromPlans({ state = {} }) {
28
39
  filterByPlanId
29
40
  );
30
41
 
31
- return planItems.map(({ fields }) => new ProductModel(fields._embedded.product));
42
+ const products = []
43
+ planItems
44
+ .map(({ fields }) => new ProductModel(fields._embedded.product))
45
+ .forEach(product => {
46
+ if (products.every(item => item.id !== product.id)) {
47
+ products.push(product)
48
+ }
49
+ });
50
+ return products;
32
51
  } catch(e) {
33
52
  // Ignore and return empty items
34
53
  };
@@ -1,3 +1,85 @@
1
1
  import BaseModel from './base-model';
2
2
 
3
- export default class PlanModel extends BaseModel {}
3
+ export class PlanPricingBracketModel {
4
+ constructor ({
5
+ maxQuantity = null,
6
+ price = null
7
+ } = {}) {
8
+ this.maxQuantity = maxQuantity || Number.MAX_SAFE_INTEGER;
9
+ this.price = !Number.isNaN(parseFloat(price)) ? parseFloat(price) : null;
10
+ }
11
+ }
12
+
13
+ export class PlanPricingModel {
14
+ static SimpleFormulas = {
15
+ fixedFee: 'fixed-fee',
16
+ flatRate: 'flat-rate'
17
+ };
18
+
19
+ static BracketFormulas = {
20
+ stairstep: 'stairstep',
21
+ tiered: 'tiered',
22
+ volume: 'volume'
23
+ };
24
+
25
+ static Formulas = {
26
+ ...PlanPricingModel.SimpleFormulas,
27
+ ...PlanPricingModel.BracketFormulas
28
+ };
29
+
30
+ constructor ({
31
+ formula = PlanPricingModel.Formulas.fixedFee,
32
+ price = 0,
33
+ maxQuantity = null,
34
+ brackets = []
35
+ } = {}) {
36
+ this.formula = formula;
37
+
38
+ switch (this.formula) {
39
+ case PlanPricingModel.Formulas.stairstep:
40
+ case PlanPricingModel.Formulas.tiered:
41
+ case PlanPricingModel.Formulas.volume:
42
+ this.brackets = brackets.map(bracket => new PlanPricingBracketModel(bracket));
43
+ break;
44
+ case PlanPricingModel.Formulas.flatRate:
45
+ this.price = !Number.isNaN(parseFloat(price)) ? parseFloat(price) : null;
46
+ this.maxQuantity = maxQuantity;
47
+ break;
48
+ case PlanPricingModel.Formulas.fixedFee:
49
+ default:
50
+ this.price = !Number.isNaN(parseFloat(price)) ? parseFloat(price) : null;
51
+ break;
52
+ }
53
+ }
54
+
55
+ get isSimple() {
56
+ return Object.values(PlanPricingModel.SimpleFormulas).includes(this.formula);
57
+ }
58
+
59
+ get isBracket () {
60
+ return Object.values(PlanPricingModel.BracketFormulas).includes(this.formula);
61
+ }
62
+
63
+ toPayload() {
64
+ return {
65
+ ...this,
66
+ isSimple: this.isSimple,
67
+ isBracket: this.isBracket,
68
+ }
69
+ }
70
+ }
71
+
72
+ export default class PlanModel extends BaseModel {
73
+ constructor(fields = {}) {
74
+ super(fields);
75
+
76
+ this.pricing = new PlanPricingModel(fields.pricing || {});
77
+ }
78
+
79
+ toPayload() {
80
+ return {
81
+ ...this,
82
+ pricing: this.pricing.toPayload(),
83
+ }
84
+ }
85
+ }
@@ -18,6 +18,10 @@ export async function fetchSummary({ data = null, state = null } = {}) {
18
18
  planId: item.planId,
19
19
  quantity: item.quantity,
20
20
  }));
21
+
22
+ if (state.data?.addons > 0) {
23
+ payload.data.items.concat(state.data.addons)
24
+ }
21
25
  }
22
26
 
23
27
  if (state.data?.amountAndCurrency) {
@@ -0,0 +1,21 @@
1
+ export function updateAddonsHandler(iframe) {
2
+ iframe.component.on(`${iframe.name}-update-addons`, async ({addons, previewPurchase}) => {
3
+ const addonPlanIds = addons.map(addon => addon.planId);
4
+ iframe.state.data.addons = addonPlanIds;
5
+ iframe.state.data.previewPurchase = previewPurchase;
6
+
7
+ iframe.state.data.previewPurchase.addonLineItems = iframe.state.data.previewPurchase.lineItems
8
+ .filter(item => addonPlanIds.includes(item.planId));
9
+ iframe.state.data.previewPurchase.lineItems = iframe.state.data.previewPurchase.lineItems
10
+ .filter(item => !addonPlanIds.includes(item.planId));
11
+
12
+ const updateModel = {
13
+ data: iframe.state.data.toPostmatesModel(),
14
+ options: iframe.state.options
15
+ }
16
+ if (iframe.state.iframeComponents.summary) {
17
+ iframe.state.iframeComponents.summary.component.call('update', updateModel);
18
+ }
19
+ iframe.state.iframeComponents.form.component.call('update', updateModel);
20
+ });
21
+ }
@@ -3,6 +3,7 @@ import BaseIframe from './base-iframe';
3
3
  import { resizeComponentHandler } from './events/resize-component-handler';
4
4
  import { dispatchEventHandler } from './events/dispatch-event-handler';
5
5
  import { updateCouponsHandler } from './events/update-coupons-handler';
6
+ import { updateAddonsHandler } from './events/update-addons-handler';
6
7
  import { showErrorHandler } from './events/show-error-handler';
7
8
  import { stopLoaderHandler } from './events/stop-loader-handler';
8
9
 
@@ -17,5 +18,6 @@ export class ViewIframe extends BaseIframe {
17
18
  stopLoaderHandler(this, { loader });
18
19
  showErrorHandler(this);
19
20
  updateCouponsHandler(this);
21
+ updateAddonsHandler(this);
20
22
  }
21
23
  }