Repository navigation
Expand file tree
/
Copy path_payment-methods.js
More file actions
386 lines (346 loc) · 16.3 KB
/
Copy path_payment-methods.js
File metadata and controls
386 lines (346 loc) · 16.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
const rcPaymentMethods = {{ payment_methods | json }};
sessionStorage.removeItem('rc_billing_countries'); // Remove old countries in case it still exists
sessionStorage.setItem('rc_countries', JSON.stringify({{ billing_countries | json }})); // Used by the country/province select utils
// IIFE to not muddy up the global context
(function () {
const { renderExpandableCard } = ReCharge.Novum.Components;
const { getAddressDom, getAssociatedSubscriptionsDom, render, createSpinner, getPaymentMethodDetailsDom } = ReCharge.Novum.DomCreators;
const { capitalize } = ReCharge.Novum.Utils;
let isApiRequestPending = false; // Is there a request pending on this page?
function findAndReplacePaymentMethod(paymentMethod) {
const idx = rcPaymentMethods.findIndex(pm => pm.id === paymentMethod.id);
rcPaymentMethods[idx] = { ...rcPaymentMethods[idx], ...paymentMethod };
}
// Creating the v-dom for payment methods
const paymentMethodCardsEl = document.createElement('div');
paymentMethodCardsEl.classList.add('payment-method-cards');
document.getElementById('PaymentMethodsPage')?.append(paymentMethodCardsEl);
// Will recalculate all the content that needs to change to not require a reload
function getDynamicPaymentMethodContent(paymentMethod) {
const paymentDetails = paymentMethod.payment_details;
const paymentType = paymentMethod.payment_type;
const brand = paymentDetails.brand.toLowerCase();
return {
address: getAddressDom(paymentMethod.billing_address),
paymentType: brand ? `${capitalize(brand)} ending in ${paymentDetails.last4}` : capitalize(paymentType.toLowerCase().replace('_', ' ')),
cardSummary: `
<span class="mr-2 d-flex">
${getPaymentMethodDetailsDom(paymentMethod)}
</span>
`
}
}
function renderPaymentMethods() {
rcPaymentMethods.forEach((paymentMethod) => {
const isShopify = paymentMethod.processor_name === 'shopify_payments';
const allowEdit = !isShopify;
const subscriptions = paymentMethod.subscriptions;
let element = document.querySelector(`.payment-method[data-payment-method-id="${paymentMethod.id}"]`);
const hasRendered = !!element;
const content = getDynamicPaymentMethodContent(paymentMethod);
// You can't remove payment methods with subscriptions or if it's the last one
const allowRemove = !subscriptions.length && rcPaymentMethods.length > 1 && !isShopify;
// Generate the element if it hasn't been rendered yet
if (!hasRendered) {
element = document.createElement('div');
element.classList.add('payment-method', 'rc-expandable-card');
element.setAttribute('data-payment-method-id', paymentMethod.id);
// Generate the payment method
element.innerHTML = `
<div class="rc-expandable-card--summary">
<span class="mr-2 d-flex align-items-center">
<span class="payment-method-logo">${getCardLogo(paymentMethod)}</span>
<span class="description flex-1">
<div class="payment-type">
${content.paymentType}
</div>
</span>
</span>
</div>
<div class="rc-expandable-card--details">
<div class="grid-250">
<div class="payment-method-details">
<h4 class="rc-subheading mt-4">Payment Method</h4>
<div class="card-summary mb-2">
${content.cardSummary}
</div>
${render(allowEdit && `
<button class="edit-payment-method rc-btn rc-btn--link" type="button">
<span class="rc-btn--icon">{% include '_edit-icon.svg' %}</span>
Edit Payment Method
</button>
`)}
</div>
<div class="billing-address-container">
<h4 class="rc-subheading mt-4">Billing Address</h4>
<div class="billing-address mb-2">${content.address}</div>
${render(allowEdit && `
<button class="edit-billing-address rc-btn rc-btn--link" type="button">
<span class="rc-btn--icon">{% include '_edit-icon.svg' %}</span>
Edit Billing Address
</button>
`)}
</div>
</div>
${getAssociatedSubscriptionsDom(subscriptions)}
${render((isShopify || allowRemove) && `
<div class="actions d-flex justify-end">
${render(allowRemove && `
<button type="button" class="remove-payment-method rc-btn rc-btn--primary-border mt-5">
Remove Payment Method
</button>
`)}
${render(isShopify && `
<button type="button" class="edit-shopify-payment-method rc-btn rc-btn--primary-border mt-5 ml-2">
Edit Payment Method
</button>
`)}
</div>
`)}
</div>
`;
renderExpandableCard(element);
paymentMethodCardsEl.append(element);
element.querySelector('.edit-payment-method')?.addEventListener('click', onEditPaymentMethod);
element.querySelector('.edit-shopify-payment-method')?.addEventListener('click', onEditShopifyPaymentMethod);
element.querySelector('.edit-billing-address')?.addEventListener('click', onEditBillingAddress);
element.querySelector('.remove-payment-method')?.addEventListener('click', onRemovePaymentMethod);
} else {
// If it has already been rendered lets just update the dynamic content
element.querySelector('.payment-method-logo').innerHTML = getCardLogo(paymentMethod);
element.querySelector('.payment-type').innerHTML = content.paymentType;
element.querySelector('.billing-address').innerHTML = content.address;
element.querySelector('.card-summary').innerHTML = content.cardSummary;
const removeActionEl = element.querySelector('.remove-payment-method');
if (removeActionEl && !allowRemove) { // If the remove action exists and we cannot remove anymore, remove the button
element.removeChild(removeActionEl);
}
}
});
const renderedPaymentMethods = document.querySelectorAll('.payment-method[data-payment-method-id]');
if (renderedPaymentMethods.length) {
// Remove all payment methods that no longer exist
renderedPaymentMethods.forEach((el) => {
if (!rcPaymentMethods.some(pm => pm.id === Number(el.getAttribute('data-payment-method-id')))) {
paymentMethodCardsEl.removeChild(el);
}
});
}
// If there are no payment methods rendered, show empty message
const emptyEl = paymentMethodCardsEl.querySelector('.empty');
if (!document.querySelector('.payment-method[data-payment-method-id]')) {
paymentMethodCardsEl.innerHTML = `<p class="empty">There are no payment methods associated with this account. Please go through checkout again to add one.</p>`;
} else if (emptyEl) {
// Remove the no shipping text if it exists and there are addresses to show
paymentMethodCardsEl.removeChild(emptyEl);
}
}
function getAddressFormData() {
return {
first_name: document.getElementById('first_name').value,
last_name: document.getElementById('last_name').value,
company: document.getElementById('company').value,
address1: document.getElementById('address1').value,
address2: document.getElementById('address2').value,
country: document.getElementById('country').value,
city: document.getElementById('city').value,
province: document.getElementById('province').value,
zip: document.getElementById('zip').value,
phone: document.getElementById('phone').value,
};
}
async function sendUpdateEmail(e) {
e.preventDefault();
if (isApiRequestPending) {
return false;
}
isApiRequestPending = true;
const buttonEl = e.target;
ReCharge.Forms.toggleButtonLoading(buttonEl);
try {
const addressId = Number(buttonEl.getAttribute('data-address-id')) || undefined;
await axios.post(ReCharge.Endpoints.send_shopify_connector_email(), {
template_type: 'shopify_update_payment_information',
type: 'email',
address_id: addressId
});
ReCharge.Toast.addToast(`{{ 'cp_toast_success' | t }}`, `{{ 'cp_update_email_sent' | t }}`);
ReCharge.Drawer.close();
} catch (error) {
let errorMessage = `{{ "cp_something_went_wrong" | t }}`;
if (error.response && error.response.data && error.response.data.error) {
errorMessage = error.response.data.error;
}
ReCharge.Forms.toggleButtonLoading(buttonEl);
ReCharge.Toast.addToast(`{{ 'cp_toast_error' | t }}`, errorMessage);
} finally {
isApiRequestPending = false;
}
}
function onEditShopifyPaymentMethod(e) {
ReCharge.Drawer.open({ header: 'Edit payment method', content: `{% include '_edit_shopify_payment_method.html' %}` });
const paymentSummaryEl = e.path.find(el => el.classList.contains('rc-expandable-card--details')).querySelector('.payment-type');
document.querySelector('.edit-shopify-payment-method-container .payment-method').innerHTML = paymentSummaryEl.innerHTML;
const paymentMethodId = getPaymentMethodIdFromEvent(e);
const addressId = rcPaymentMethods.find(pm => pm.id === paymentMethodId)?.subscriptions[0]?.address_id
const sendButton = document.querySelector('.shopify-send-update-email');
if (addressId) {
sendButton.setAttribute('data-address-id', addressId);
}
sendButton.addEventListener('click', sendUpdateEmail);
}
async function saveBillingAddress(e) {
e.preventDefault();
if (isApiRequestPending) {
return false;
}
isApiRequestPending = true;
const buttonEl = e.target.querySelector('.update-billing-address');
ReCharge.Forms.toggleButtonLoading(buttonEl);
try {
const id = Number(e.target.getAttribute('data-payment-method-id'));
const { data: { payment_method } } = await axios.post(ReCharge.Endpoints.payment_methods(id), {
billing_address: getAddressFormData()
});
findAndReplacePaymentMethod(payment_method);
renderPaymentMethods();
ReCharge.Toast.addToast(`{{ 'cp_toast_success' | t }}`, 'Billing address updated successfully');
ReCharge.Drawer.close();
} catch (error) {
let errorMessage = `{{ "cp_something_went_wrong" | t }}`;
if (error.response && error.response.data && error.response.data.error) {
errorMessage = error.response.data.error;
}
ReCharge.Forms.toggleButtonLoading(buttonEl);
ReCharge.Toast.addToast(`{{ 'cp_toast_error' | t }}`, errorMessage);
} finally {
isApiRequestPending = false;
}
return false;
}
function getPaymentMethodIdFromEvent(e) {
return Number(e.path.find(el => el.getAttribute('data-payment-method-id')).getAttribute('data-payment-method-id')); // Get the current payment method id
}
function onEditBillingAddress(e) {
const id = getPaymentMethodIdFromEvent(e);
const paymentMethod = rcPaymentMethods.find(pm => pm.id === id);
const numOfSubs = paymentMethod.subscriptions.length;
const subscriptionText = numOfSubs === 1 ? `1 associated subscription` : `${numOfSubs} associated subscriptions`;
ReCharge.Drawer.open({
header: 'Edit billing address',
content: `
<form action="" method="post" id="Recharge_Address_Form">
<p class="subs-update-text rc-subtext text-center mb-5">
Updating this payment method will update ${subscriptionText}.
</p>
<h3 class="rc-subheading">Billing Address</h3>
{% include '_address_fields.html' %}
<button type="submit" class="update-billing-address rc-btn rc-btn--primary">
Update
</button>
</form>
` });
// Update all the values to be what is current used
ReCharge.Forms.populateAddressData(paymentMethod.billing_address);
// Update the countries/province dropdowns
ReCharge.Forms.buildCountries('billing');
ReCharge.Forms.updateProvinces(document.getElementById('country'));
// Add id to form to easily get current payment method
document.forms.Recharge_Address_Form.setAttribute('data-payment-method-id', id);
// Add submit handler
document.getElementById('Recharge_Address_Form').addEventListener('submit', saveBillingAddress);
}
function onRemovePaymentMethod(e) {
const id = getPaymentMethodIdFromEvent(e);
const paymentMethod = rcPaymentMethods.find(pm => pm.id === id);
// If we are a shopify payment method, submit an email to remove
if (paymentMethod.processor_name === 'shopify_payments') {
onEditShopifyPaymentMethod(e);
ReCharge.Drawer.setHeader('Remove payment method');
return;
}
ReCharge.Modal.open({
title: 'Remove payment method?',
content: 'Are you sure you want to remove this payment method?',
cancelBtnText: 'Cancel',
confirmBtnText: 'Remove',
onConfirm: async (e) => {
if (isApiRequestPending) {
return false;
}
isApiRequestPending = true;
const buttonEl = e.target;
ReCharge.Forms.toggleButtonLoading(buttonEl);
try {
await axios.delete(ReCharge.Endpoints.payment_methods(id));
// Remove paymentMethod from the cache and then syncing the dom
const idx = rcPaymentMethods.findIndex(pm => pm.id === id);
rcPaymentMethods.splice(idx, 1);
renderPaymentMethods();
ReCharge.Toast.addToast(`{{ 'cp_toast_success' | t }}`, 'Payment method successfully removed');
ReCharge.Modal.close();
} catch (error) {
let errorMessage = `{{ "cp_something_went_wrong" | t }}`;
if (error.response && error.response.data && error.response.data.error) {
errorMessage = error.response.data.error;
}
ReCharge.Forms.toggleButtonLoading(buttonEl);
ReCharge.Toast.addToast(`{{ 'cp_toast_error' | t }}`, errorMessage);
} finally {
isApiRequestPending = false;
}
},
});
}
function addCardWindowListener(onSuccess) {
const listener = (event) => {
if (
event.origin.includes('shopifysubscriptions.com') ||
event.origin.includes('.rechargeapps.com')
) {
if (event.data?.billingComplete) {
onSuccess(event.data.paymentMethodId);
window.removeEventListener('message', listener);
}
}
return;
}
window.addEventListener('message', listener, false);
}
function createDrawerSpinner() {
const drawerSpinner = createSpinner({ size: 50 });
drawerSpinner.style.position = 'absolute';
drawerSpinner.style.top = '24px';
drawerSpinner.style.left = 'calc(50% - 25px)';
return drawerSpinner;
}
function onEditPaymentMethod(e) {
const id = getPaymentMethodIdFromEvent(e);
// Get the card url and add the id to it before the query
let url = '{{ payment_methods_card_form_url }}';
url = url.includes('?') ? url.replace('?', `/${id}?`) : `${url}/${id}`;
ReCharge.Drawer.open({ header: 'Edit Payment Method', content: `<iframe src="${url}" id="customer-card-form" name="customer-card-form" frameborder="0" allowtransparency="true" style="display: none;"></iframe>` });
// Get the frame and setup a spinner to display while loading
const frameEl = document.getElementById('customer-card-form');
const drawerSpinner = createDrawerSpinner();
frameEl.insertAdjacentElement('afterend', drawerSpinner);
frameEl.addEventListener('load', () => {
// Hide the spinner and show the frame once it's loaded
drawerSpinner.style.display = 'none';
frameEl.style.display = 'block';
});
// When the card update is successful, try to fetch it and rerender. Otherwise reload the page as a fallback
addCardWindowListener(async () => {
ReCharge.Toast.addToast(`{{ 'cp_toast_success' | t }}`, 'Payment method updated successfully');
ReCharge.Drawer.close();
try {
const { data: { payment_method } } = await axios.get(ReCharge.Endpoints.payment_methods(id));
findAndReplacePaymentMethod(payment_method);
renderPaymentMethods();
} catch (e) {
window.location.reload();
}
});
}
renderPaymentMethods();
})();