@designcrowd/library.brand-page 6.2.4 → 6.2.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.
Files changed (61) hide show
  1. package/lib/src/{CreatedOnBrandCrowd-de-de-ystrO2LW.js → CreatedOnBrandCrowd-de-de-CkJM0rEy.js} +1 -1
  2. package/lib/src/{CreatedOnBrandCrowd-en-us-BU7hNzTZ.js → CreatedOnBrandCrowd-en-us-CKOotMQK.js} +1 -1
  3. package/lib/src/{CreatedOnBrandCrowd-es-es-CbuxneQ3.js → CreatedOnBrandCrowd-es-es-mrDI8AaY.js} +1 -1
  4. package/lib/src/{CreatedOnBrandCrowd-fr-fr-BuNUGOFr.js → CreatedOnBrandCrowd-fr-ca-DtmVXezM.js} +1 -1
  5. package/lib/src/{CreatedOnBrandCrowd-fr-ca-BuNUGOFr.js → CreatedOnBrandCrowd-fr-fr-DtmVXezM.js} +1 -1
  6. package/lib/src/{CreatedOnBrandCrowd-pt-pt-BehL88mc.js → CreatedOnBrandCrowd-pt-br-C8Cy74df.js} +1 -1
  7. package/lib/src/{CreatedOnBrandCrowd-pt-br-BehL88mc.js → CreatedOnBrandCrowd-pt-pt-C8Cy74df.js} +1 -1
  8. package/lib/src/{CreatedOnDcom-de-de-CMxjFZt9.js → CreatedOnDcom-de-de-FVl5OqQM.js} +1 -1
  9. package/lib/src/{CreatedOnDcom-en-us-2Cf6WBvx.js → CreatedOnDcom-en-us-e0s7gZGG.js} +1 -1
  10. package/lib/src/{CreatedOnDcom-es-es-CATNnbTq.js → CreatedOnDcom-es-es-BXMekiTg.js} +1 -1
  11. package/lib/src/{CreatedOnDcom-fr-ca-DQtMfB-a.js → CreatedOnDcom-fr-ca-74Vm0wzS.js} +1 -1
  12. package/lib/src/{CreatedOnDcom-fr-fr-C1_FWOQf.js → CreatedOnDcom-fr-fr-C6TT73Rw.js} +1 -1
  13. package/lib/src/{CreatedOnDcom-pt-br-C0QOCW0L.js → CreatedOnDcom-pt-br-Drg2eaLk.js} +1 -1
  14. package/lib/src/{CreatedOnDcom-pt-pt-lHD-p6o4.js → CreatedOnDcom-pt-pt-BgsEIMfv.js} +1 -1
  15. package/lib/src/bundled-translations-BNA5h0Dx.js +179 -0
  16. package/lib/src/bundled-translations.de-DE-fyTHrRxT.js +179 -0
  17. package/lib/src/bundled-translations.es-ES-BMmvJGGC.js +179 -0
  18. package/lib/src/bundled-translations.fr-CA-Bo69L_BP.js +179 -0
  19. package/lib/src/bundled-translations.fr-FR-C4yTVFOW.js +179 -0
  20. package/lib/src/bundled-translations.pt-BR-CPMPfny0.js +179 -0
  21. package/lib/src/bundled-translations.pt-PT-C6ZwYs37.js +179 -0
  22. package/lib/src/bundles/bundled-translations.de-DE.json +5 -1
  23. package/lib/src/bundles/bundled-translations.es-ES.json +5 -1
  24. package/lib/src/bundles/bundled-translations.fr-CA.json +5 -1
  25. package/lib/src/bundles/bundled-translations.fr-FR.json +5 -1
  26. package/lib/src/bundles/bundled-translations.json +5 -1
  27. package/lib/src/bundles/bundled-translations.pt-BR.json +5 -1
  28. package/lib/src/bundles/bundled-translations.pt-PT.json +5 -1
  29. package/lib/src/css/library.brand-page-brandCrowd.min.css +1 -1
  30. package/lib/src/css/library.brand-page-brandPage.min.css +1 -1
  31. package/lib/src/css/library.brand-page-designCom.min.css +1 -1
  32. package/lib/src/css/library.brand-page-preview.min.css +1 -1
  33. package/lib/src/index.mjs +1 -1
  34. package/lib/src/{lib-CqHUUPUW.js → lib--ZDYWLFf.js} +1165 -1160
  35. package/package.json +1 -1
  36. package/src/brand-page/components/payment/RequestPaymentStripe.vue +15 -6
  37. package/src/brand-page/components/payment/RequestPaymentStripe.vue.test.ts +644 -0
  38. package/src/brand-page/components/payment/i18n/request-payment-stripe.de-DE.json +5 -1
  39. package/src/brand-page/components/payment/i18n/request-payment-stripe.es-ES.json +5 -1
  40. package/src/brand-page/components/payment/i18n/request-payment-stripe.fr-CA.json +5 -1
  41. package/src/brand-page/components/payment/i18n/request-payment-stripe.fr-FR.json +5 -1
  42. package/src/brand-page/components/payment/i18n/request-payment-stripe.json +5 -1
  43. package/src/brand-page/components/payment/i18n/request-payment-stripe.pt-BR.json +5 -1
  44. package/src/brand-page/components/payment/i18n/request-payment-stripe.pt-PT.json +5 -1
  45. package/src/bundles/bundled-translations.de-DE.json +5 -1
  46. package/src/bundles/bundled-translations.es-ES.json +5 -1
  47. package/src/bundles/bundled-translations.fr-CA.json +5 -1
  48. package/src/bundles/bundled-translations.fr-FR.json +5 -1
  49. package/src/bundles/bundled-translations.json +5 -1
  50. package/src/bundles/bundled-translations.pt-BR.json +5 -1
  51. package/src/bundles/bundled-translations.pt-PT.json +5 -1
  52. package/lib/src/bundled-translations-BYUfWBT3.js +0 -179
  53. package/lib/src/bundled-translations.de-DE-MmzUu1tt.js +0 -179
  54. package/lib/src/bundled-translations.es-ES-D-tG33XL.js +0 -179
  55. package/lib/src/bundled-translations.fr-CA-CNc1H1h3.js +0 -179
  56. package/lib/src/bundled-translations.fr-FR-DX0-yepT.js +0 -179
  57. package/lib/src/bundled-translations.pt-BR-ih1-LRgy.js +0 -179
  58. package/lib/src/bundled-translations.pt-PT-Dq2CmD1p.js +0 -179
  59. package/lib/src/metadata-builder/builders/reorder-pages-builder.d.ts +0 -13
  60. package/lib/src/metadata-builder/builders/reorder-pages-builder.js +0 -49
  61. package/src/metadata-builder/builders/reorder-pages-builder.ts +0 -80
@@ -0,0 +1,644 @@
1
+ import { enableAutoUnmount, flushPromises, mount } from '@vue/test-utils';
2
+ import type { DOMWrapper, VueWrapper } from '@vue/test-utils';
3
+ import { AxiosError, AxiosHeaders } from 'axios';
4
+ import i18next from 'i18next';
5
+ import { TextInput } from '@designcrowd/fe-shared-lib';
6
+ import {
7
+ setLocaleAsync,
8
+ supportedLocales,
9
+ type LocaleTypes,
10
+ } from '@src/composables/useTranslate';
11
+ import brandPageClient from '../../clients/brand-page.client';
12
+ import {
13
+ ButtonStyles,
14
+ type BrandPagePaymentConfig,
15
+ type CreateCartResult,
16
+ type StripePaymentIntent,
17
+ } from '../../models';
18
+ import RequestPaymentStripe from './RequestPaymentStripe.vue';
19
+ import en from './i18n/request-payment-stripe.json';
20
+ import deDE from './i18n/request-payment-stripe.de-DE.json';
21
+ import esES from './i18n/request-payment-stripe.es-ES.json';
22
+ import frCA from './i18n/request-payment-stripe.fr-CA.json';
23
+ import frFR from './i18n/request-payment-stripe.fr-FR.json';
24
+ import ptBR from './i18n/request-payment-stripe.pt-BR.json';
25
+ import ptPT from './i18n/request-payment-stripe.pt-PT.json';
26
+
27
+ vi.mock('../../clients/brand-page.client', () => ({
28
+ default: { createStripePaymentIntent: vi.fn() },
29
+ }));
30
+
31
+ enableAutoUnmount(afterEach);
32
+
33
+ // Expected values come from the locale files, not literals, so Smartling's
34
+ // rewrite of the wording can't turn these tests red.
35
+ const localeFiles: Record<LocaleTypes, typeof en> = {
36
+ 'en-US': en,
37
+ 'de-DE': deDE,
38
+ 'es-ES': esES,
39
+ 'fr-CA': frCA,
40
+ 'fr-FR': frFR,
41
+ 'pt-BR': ptBR,
42
+ 'pt-PT': ptPT,
43
+ };
44
+
45
+ const stringsFor = (locale: LocaleTypes): (typeof en)['requestPaymentStripe'] =>
46
+ localeFiles[locale].requestPaymentStripe;
47
+
48
+ const NON_ENGLISH_LOCALES = supportedLocales.filter(
49
+ (locale) => locale !== 'en-US',
50
+ );
51
+
52
+ // R2 and R3 keep today's English word for word, so these stay literal.
53
+ const ENGLISH_HINTS = {
54
+ cardNumber: 'Credit Card Number',
55
+ cardExpiry: 'Expiry',
56
+ cardCvc: 'CVV',
57
+ };
58
+ const ENGLISH_FAILURE_MESSAGE =
59
+ 'Unable to create payment intent. Please try again.';
60
+
61
+ const CARD_FORM_KEYS = [
62
+ 'cardNumberPlaceholder',
63
+ 'cardExpiryPlaceholder',
64
+ 'cardCvcPlaceholder',
65
+ 'paymentIntentErrorMessage',
66
+ ] as const;
67
+
68
+ // See RecaptchaDisclosure.vue.test.ts: the shared i18next singleton only takes
69
+ // the language from `setLocaleAsync` on its first call.
70
+ const useLocale = async (locale: string): Promise<void> => {
71
+ await setLocaleAsync(locale as LocaleTypes);
72
+ await i18next.changeLanguage(locale);
73
+ };
74
+
75
+ type ChangeListener = (response: {
76
+ elementType: string;
77
+ complete: boolean;
78
+ }) => void;
79
+
80
+ interface FakeElement {
81
+ type: string;
82
+ options: { placeholder?: string };
83
+ mount: ReturnType<typeof vi.fn>;
84
+ addEventListener: (event: string, listener: ChangeListener) => void;
85
+ change?: ChangeListener;
86
+ }
87
+
88
+ interface FakeStripe {
89
+ Stripe: ReturnType<typeof vi.fn>;
90
+ elements: Record<string, FakeElement>;
91
+ confirmCardPayment: ReturnType<typeof vi.fn>;
92
+ }
93
+
94
+ const createFakeStripe = (): FakeStripe => {
95
+ const elements: Record<string, FakeElement> = {};
96
+ const confirmCardPayment = vi.fn();
97
+
98
+ const Stripe = vi.fn(() => ({
99
+ elements: () => ({
100
+ create: (type: string, options: FakeElement['options']): FakeElement => {
101
+ const element: FakeElement = {
102
+ type,
103
+ options,
104
+ mount: vi.fn(),
105
+ addEventListener: (event: string, listener: ChangeListener): void => {
106
+ if (event === 'change') {
107
+ element.change = listener;
108
+ }
109
+ },
110
+ };
111
+
112
+ elements[type] = element;
113
+
114
+ return element;
115
+ },
116
+ }),
117
+ confirmCardPayment,
118
+ }));
119
+
120
+ return { Stripe, elements, confirmCardPayment };
121
+ };
122
+
123
+ let fakeStripe: FakeStripe;
124
+
125
+ const setWindowStripe = (value?: unknown): void => {
126
+ (window as { Stripe?: unknown }).Stripe = value;
127
+ };
128
+
129
+ const errorHandler = vi.fn();
130
+ const createStripePaymentIntent = vi.mocked(
131
+ brandPageClient.createStripePaymentIntent,
132
+ );
133
+
134
+ const baseProps = {
135
+ cart: {
136
+ cartId: 'cart-1',
137
+ paymentProviderName: 'Stripe',
138
+ paymentProviderExternalId: 'acct_test',
139
+ totalAmount: 2500,
140
+ } as CreateCartResult<StripePaymentIntent>,
141
+ checkoutInformation: {},
142
+ buttonStyles: new ButtonStyles(),
143
+ paymentConfig: {
144
+ defaultCurrency: 'USD',
145
+ defaultCurrencySymbol: '$',
146
+ } as BrandPagePaymentConfig,
147
+ stripePublishableKey: 'pk_test_123',
148
+ };
149
+
150
+ const consumerProps = {
151
+ Payment: { isShop: false, productName: 'Logo design' },
152
+ Tipping: { isShop: false },
153
+ Store: { isShop: true, isShippingEnabled: true },
154
+ };
155
+
156
+ const mountForm = (
157
+ props: Partial<{
158
+ productName: string;
159
+ isShop: boolean;
160
+ isShippingEnabled: boolean;
161
+ }> = {},
162
+ ): VueWrapper =>
163
+ mount(RequestPaymentStripe, {
164
+ props: { ...baseProps, ...props },
165
+ global: { config: { errorHandler } },
166
+ });
167
+
168
+ const hints = (): Record<string, string | undefined> => ({
169
+ cardNumber: fakeStripe.elements.cardNumber?.options.placeholder,
170
+ cardExpiry: fakeStripe.elements.cardExpiry?.options.placeholder,
171
+ cardCvc: fakeStripe.elements.cardCvc?.options.placeholder,
172
+ });
173
+
174
+ const hintsFor = (locale: LocaleTypes): Record<string, string> => ({
175
+ cardNumber: stringsFor(locale).cardNumberPlaceholder,
176
+ cardExpiry: stringsFor(locale).cardExpiryPlaceholder,
177
+ cardCvc: stringsFor(locale).cardCvcPlaceholder,
178
+ });
179
+
180
+ const payButton = (wrapper: VueWrapper): DOMWrapper<Element> =>
181
+ wrapper.find('.bp-style-wrapper button');
182
+
183
+ const formErrors = (wrapper: VueWrapper): string[] =>
184
+ wrapper.findAll('.form-error').map((error) => error.text());
185
+
186
+ const completeForm = async (wrapper: VueWrapper): Promise<void> => {
187
+ Object.values(fakeStripe.elements).forEach((element) => {
188
+ element.change?.({ elementType: element.type, complete: true });
189
+ });
190
+ await wrapper
191
+ .findComponent(TextInput)
192
+ .find('input')
193
+ .setValue('visitor@example.com');
194
+ await flushPromises();
195
+ };
196
+
197
+ const clickPay = async (wrapper: VueWrapper): Promise<void> => {
198
+ expect(payButton(wrapper).attributes('disabled')).toBeUndefined();
199
+ await payButton(wrapper).trigger('click');
200
+ await flushPromises();
201
+ };
202
+
203
+ const paymentIntent = {
204
+ paymentIntent: { client_secret: 'pi_test_secret' } as StripePaymentIntent,
205
+ };
206
+
207
+ const httpError = (status: number): AxiosError =>
208
+ new AxiosError(
209
+ `Request failed with status code ${status}`,
210
+ status >= 500 ? AxiosError.ERR_BAD_RESPONSE : AxiosError.ERR_BAD_REQUEST,
211
+ undefined,
212
+ undefined,
213
+ {
214
+ status,
215
+ statusText: '',
216
+ data: {},
217
+ headers: {},
218
+ config: { headers: new AxiosHeaders() },
219
+ },
220
+ );
221
+
222
+ const networkError = (): AxiosError =>
223
+ new AxiosError('Network Error', AxiosError.ERR_NETWORK);
224
+
225
+ const FAILED_REQUESTS: [string, () => AxiosError][] = [
226
+ ['refused (HTTP 400)', () => httpError(400)],
227
+ ['server error (HTTP 500)', () => httpError(500)],
228
+ ['no connection', networkError],
229
+ ];
230
+
231
+ // Stripe writes its own decline text; the form shows it as given.
232
+ const STRIPE_DECLINE = {
233
+ type: 'card_error',
234
+ code: 'card_declined',
235
+ decline_code: 'generic_decline',
236
+ message: 'Your card was declined.',
237
+ };
238
+
239
+ const FAILED_INTENT_STEPS: [string, () => void][] = [
240
+ [
241
+ 'no payment details',
242
+ () => createStripePaymentIntent.mockResolvedValueOnce(null),
243
+ ],
244
+ [
245
+ 'a failed request (HTTP 500)',
246
+ () => createStripePaymentIntent.mockRejectedValueOnce(httpError(500)),
247
+ ],
248
+ ];
249
+
250
+ describe('RequestPaymentStripe', () => {
251
+ beforeEach(() => {
252
+ fakeStripe = createFakeStripe();
253
+ setWindowStripe(fakeStripe.Stripe);
254
+ errorHandler.mockReset();
255
+ createStripePaymentIntent.mockReset();
256
+ vi.spyOn(console, 'error').mockImplementation(() => undefined);
257
+ });
258
+
259
+ afterEach(() => {
260
+ setWindowStripe(undefined);
261
+ vi.useRealTimers();
262
+ vi.restoreAllMocks();
263
+ });
264
+
265
+ describe('card-field hints', () => {
266
+ test.each(NON_ENGLISH_LOCALES)(
267
+ 'card-field hints use the %s locale file',
268
+ async (locale) => {
269
+ await useLocale(locale);
270
+
271
+ mountForm(consumerProps.Payment);
272
+
273
+ expect(hints()).toEqual(hintsFor(locale));
274
+ expect(hints().cardNumber).not.toBe(
275
+ en.requestPaymentStripe.cardNumberPlaceholder,
276
+ );
277
+ expect(hints().cardExpiry).not.toBe(
278
+ en.requestPaymentStripe.cardExpiryPlaceholder,
279
+ );
280
+ Object.values(hints()).forEach((hint) => {
281
+ expect(hint).not.toMatch(/^requestPaymentStripe\./);
282
+ });
283
+ },
284
+ );
285
+
286
+ // A key missing from one locale file renders as its key path on the site;
287
+ // the shared test singleton would hide it behind the en-US fallback.
288
+ test.each(supportedLocales)(
289
+ 'every locale file defines the card-form keys: %s',
290
+ (locale) => {
291
+ CARD_FORM_KEYS.forEach((key) => {
292
+ const value: unknown = stringsFor(locale)[key];
293
+
294
+ expect(typeof value, `${locale} › ${key}`).toBe('string');
295
+ expect((value as string).trim(), `${locale} › ${key}`).not.toBe('');
296
+ });
297
+ },
298
+ );
299
+
300
+ test.each(Object.keys(consumerProps) as (keyof typeof consumerProps)[])(
301
+ 'hints are translated when opened from %s',
302
+ async (consumer) => {
303
+ await useLocale('de-DE');
304
+
305
+ const wrapper = mountForm(consumerProps[consumer]);
306
+
307
+ expect(hints()).toEqual(hintsFor('de-DE'));
308
+
309
+ if (consumer === 'Store') {
310
+ expect(
311
+ wrapper
312
+ .find('[data-component="payment-shipping-address"]')
313
+ .exists(),
314
+ ).toBe(true);
315
+ }
316
+ },
317
+ );
318
+
319
+ test('hints are translated when Stripe loads after mount', async () => {
320
+ await useLocale('de-DE');
321
+ setWindowStripe(undefined);
322
+ vi.useFakeTimers();
323
+
324
+ mountForm(consumerProps.Payment);
325
+
326
+ expect(fakeStripe.elements.cardNumber).toBeUndefined();
327
+
328
+ setWindowStripe(fakeStripe.Stripe);
329
+ vi.advanceTimersByTime(250);
330
+
331
+ expect(hints()).toEqual(hintsFor('de-DE'));
332
+ });
333
+
334
+ test.each([
335
+ ['de-DE', 'en-US', hintsFor('de-DE')],
336
+ ['en-US', 'de-DE', ENGLISH_HINTS],
337
+ ] as [LocaleTypes, string, Record<string, string>][])(
338
+ 'hints follow the site language, not the browser language: site %s, browser %s',
339
+ async (siteLocale, browserLocale, expected) => {
340
+ vi.spyOn(window.navigator, 'language', 'get').mockReturnValue(
341
+ browserLocale,
342
+ );
343
+ vi.spyOn(window.navigator, 'languages', 'get').mockReturnValue([
344
+ browserLocale,
345
+ ]);
346
+ await useLocale(siteLocale);
347
+
348
+ mountForm(consumerProps.Payment);
349
+
350
+ expect(hints()).toEqual(expected);
351
+ },
352
+ );
353
+
354
+ test('English site keeps the current hints word for word', async () => {
355
+ await useLocale('en-US');
356
+
357
+ const wrapper = mountForm(consumerProps.Payment);
358
+
359
+ expect(hints()).toEqual(ENGLISH_HINTS);
360
+ expect(wrapper.findAll('p.tw-font-bold').map((p) => p.text())).toContain(
361
+ 'Card Number',
362
+ );
363
+ });
364
+ });
365
+
366
+ // `setLocaleAsync` falls back to en-US for a language it doesn't support.
367
+ test.each(FAILED_INTENT_STEPS)(
368
+ 'unsupported site language falls back to English: %s',
369
+ async (_outcome, failIntentStep) => {
370
+ await useLocale('it-IT');
371
+
372
+ const wrapper = mountForm(consumerProps.Payment);
373
+
374
+ expect(hints()).toEqual(ENGLISH_HINTS);
375
+
376
+ await completeForm(wrapper);
377
+ failIntentStep();
378
+ await clickPay(wrapper);
379
+
380
+ expect(formErrors(wrapper)).toEqual([ENGLISH_FAILURE_MESSAGE]);
381
+ },
382
+ );
383
+
384
+ describe('payment-failure message', () => {
385
+ test.each(NON_ENGLISH_LOCALES)(
386
+ 'payment-failure message uses the %s locale file',
387
+ async (locale) => {
388
+ await useLocale(locale);
389
+
390
+ const wrapper = mountForm(consumerProps.Payment);
391
+
392
+ await completeForm(wrapper);
393
+ createStripePaymentIntent.mockResolvedValueOnce(null);
394
+ await clickPay(wrapper);
395
+
396
+ expect(formErrors(wrapper)).toEqual([
397
+ stringsFor(locale).paymentIntentErrorMessage,
398
+ ]);
399
+ expect(formErrors(wrapper)[0]).not.toBe(
400
+ en.requestPaymentStripe.paymentIntentErrorMessage,
401
+ );
402
+ },
403
+ );
404
+
405
+ test.each(FAILED_INTENT_STEPS)(
406
+ 'English site shows the payment-failure message word for word: %s',
407
+ async (_outcome, failIntentStep) => {
408
+ await useLocale('en-US');
409
+
410
+ const wrapper = mountForm(consumerProps.Payment);
411
+
412
+ await completeForm(wrapper);
413
+ failIntentStep();
414
+ await clickPay(wrapper);
415
+
416
+ expect(formErrors(wrapper)).toEqual([ENGLISH_FAILURE_MESSAGE]);
417
+ },
418
+ );
419
+
420
+ test.each(
421
+ NON_ENGLISH_LOCALES.flatMap((locale) =>
422
+ FAILED_REQUESTS.map(([kind, error]) => [locale, kind, error] as const),
423
+ ),
424
+ )(
425
+ 'a failed intent request shows the translated message: %s, %s',
426
+ async (locale, _kind, error) => {
427
+ const failure = error();
428
+
429
+ await useLocale(locale);
430
+
431
+ const wrapper = mountForm(consumerProps.Payment);
432
+
433
+ await completeForm(wrapper);
434
+ createStripePaymentIntent.mockRejectedValueOnce(failure);
435
+ await clickPay(wrapper);
436
+
437
+ expect
438
+ .soft(formErrors(wrapper))
439
+ .toEqual([stringsFor(locale).paymentIntentErrorMessage]);
440
+ expect.soft(payButton(wrapper).attributes('disabled')).toBeUndefined();
441
+ expect.soft(fakeStripe.confirmCardPayment).not.toHaveBeenCalled();
442
+ expect.soft(errorHandler).not.toHaveBeenCalled();
443
+ expect.soft(console.error).toHaveBeenCalledWith(failure);
444
+ },
445
+ );
446
+
447
+ // The card may already be charged once the intent exists, so a "try again"
448
+ // message there could invite a second payment.
449
+ test('a failure after the intent step shows no retry message', async () => {
450
+ const stripeFailure = new Error('confirmCardPayment failed');
451
+
452
+ await useLocale('de-DE');
453
+
454
+ const wrapper = mountForm(consumerProps.Payment);
455
+
456
+ await completeForm(wrapper);
457
+ createStripePaymentIntent.mockResolvedValueOnce(paymentIntent);
458
+ fakeStripe.confirmCardPayment.mockRejectedValueOnce(stripeFailure);
459
+ await clickPay(wrapper);
460
+
461
+ expect(formErrors(wrapper)).toEqual([]);
462
+ expect(payButton(wrapper).attributes('disabled')).toBeUndefined();
463
+ expect(errorHandler).toHaveBeenCalledTimes(1);
464
+ expect(errorHandler.mock.calls[0][0]).toBe(stripeFailure);
465
+ });
466
+
467
+ test('a failure after the intent step clears the earlier retry message', async () => {
468
+ const stripeFailure = new Error('confirmCardPayment failed');
469
+
470
+ await useLocale('de-DE');
471
+
472
+ const wrapper = mountForm(consumerProps.Payment);
473
+
474
+ await completeForm(wrapper);
475
+ createStripePaymentIntent.mockRejectedValueOnce(httpError(500));
476
+ await clickPay(wrapper);
477
+
478
+ expect(formErrors(wrapper)).toEqual([
479
+ stringsFor('de-DE').paymentIntentErrorMessage,
480
+ ]);
481
+
482
+ createStripePaymentIntent.mockResolvedValueOnce(paymentIntent);
483
+ fakeStripe.confirmCardPayment.mockRejectedValueOnce(stripeFailure);
484
+ await clickPay(wrapper);
485
+
486
+ expect(formErrors(wrapper)).toEqual([]);
487
+ expect(payButton(wrapper).attributes('disabled')).toBeUndefined();
488
+ expect(errorHandler).toHaveBeenCalledTimes(1);
489
+ expect(errorHandler.mock.calls[0][0]).toBe(stripeFailure);
490
+ });
491
+
492
+ test('retry after the payment-failure message: failing again keeps one message', async () => {
493
+ await useLocale('de-DE');
494
+
495
+ const wrapper = mountForm(consumerProps.Payment);
496
+
497
+ await completeForm(wrapper);
498
+ createStripePaymentIntent.mockRejectedValueOnce(httpError(500));
499
+ await clickPay(wrapper);
500
+
501
+ expect(formErrors(wrapper)).toEqual([
502
+ stringsFor('de-DE').paymentIntentErrorMessage,
503
+ ]);
504
+
505
+ createStripePaymentIntent.mockRejectedValueOnce(httpError(500));
506
+ await clickPay(wrapper);
507
+
508
+ expect(createStripePaymentIntent).toHaveBeenCalledTimes(2);
509
+ expect(formErrors(wrapper)).toEqual([
510
+ stringsFor('de-DE').paymentIntentErrorMessage,
511
+ ]);
512
+ });
513
+
514
+ test('retry after the payment-failure message: succeeding emits success', async () => {
515
+ await useLocale('de-DE');
516
+
517
+ const wrapper = mountForm(consumerProps.Payment);
518
+
519
+ await completeForm(wrapper);
520
+ createStripePaymentIntent.mockRejectedValueOnce(httpError(500));
521
+ await clickPay(wrapper);
522
+
523
+ expect(formErrors(wrapper)).toEqual([
524
+ stringsFor('de-DE').paymentIntentErrorMessage,
525
+ ]);
526
+
527
+ const confirmed = { paymentIntent: { status: 'succeeded' } };
528
+
529
+ createStripePaymentIntent.mockResolvedValueOnce(paymentIntent);
530
+ fakeStripe.confirmCardPayment.mockResolvedValueOnce(confirmed);
531
+ await clickPay(wrapper);
532
+
533
+ expect(wrapper.emitted('success')).toEqual([[{ result: confirmed }]]);
534
+ expect(errorHandler).not.toHaveBeenCalled();
535
+ });
536
+
537
+ test('a successful payment emits success and shows no message', async () => {
538
+ await useLocale('de-DE');
539
+
540
+ const wrapper = mountForm(consumerProps.Payment);
541
+
542
+ await completeForm(wrapper);
543
+
544
+ const confirmed = { paymentIntent: { status: 'succeeded' } };
545
+
546
+ createStripePaymentIntent.mockResolvedValueOnce(paymentIntent);
547
+ fakeStripe.confirmCardPayment.mockResolvedValueOnce(confirmed);
548
+ await clickPay(wrapper);
549
+
550
+ expect(wrapper.emitted('success')).toEqual([[{ result: confirmed }]]);
551
+ expect(formErrors(wrapper)).toEqual([]);
552
+ expect(errorHandler).not.toHaveBeenCalled();
553
+ });
554
+
555
+ test("Stripe's decline text shows word for word, not ours", async () => {
556
+ await useLocale('de-DE');
557
+
558
+ const wrapper = mountForm(consumerProps.Payment);
559
+
560
+ await completeForm(wrapper);
561
+ createStripePaymentIntent.mockResolvedValueOnce(paymentIntent);
562
+ fakeStripe.confirmCardPayment.mockResolvedValueOnce({
563
+ error: STRIPE_DECLINE,
564
+ });
565
+ await clickPay(wrapper);
566
+
567
+ expect(formErrors(wrapper)).toEqual([STRIPE_DECLINE.message]);
568
+ expect(payButton(wrapper).attributes('disabled')).toBeUndefined();
569
+ expect(wrapper.emitted('success')).toBeUndefined();
570
+ expect(errorHandler).not.toHaveBeenCalled();
571
+ });
572
+
573
+ test("Stripe's decline text clears on the next Pay click and shows again if declined again", async () => {
574
+ await useLocale('de-DE');
575
+
576
+ const wrapper = mountForm(consumerProps.Payment);
577
+
578
+ await completeForm(wrapper);
579
+ createStripePaymentIntent.mockResolvedValueOnce(paymentIntent);
580
+ fakeStripe.confirmCardPayment.mockResolvedValueOnce({
581
+ error: STRIPE_DECLINE,
582
+ });
583
+ await clickPay(wrapper);
584
+
585
+ expect(formErrors(wrapper)).toEqual([STRIPE_DECLINE.message]);
586
+
587
+ let answerIntent: (response: typeof paymentIntent) => void = () =>
588
+ undefined;
589
+
590
+ createStripePaymentIntent.mockReturnValueOnce(
591
+ new Promise((resolve) => {
592
+ answerIntent = resolve;
593
+ }),
594
+ );
595
+ fakeStripe.confirmCardPayment.mockResolvedValueOnce({
596
+ error: STRIPE_DECLINE,
597
+ });
598
+ await clickPay(wrapper);
599
+
600
+ expect(formErrors(wrapper)).toEqual([]);
601
+
602
+ answerIntent(paymentIntent);
603
+ await flushPromises();
604
+
605
+ expect(fakeStripe.confirmCardPayment).toHaveBeenCalledTimes(2);
606
+ expect(formErrors(wrapper)).toEqual([STRIPE_DECLINE.message]);
607
+ });
608
+
609
+ // Both clicks land before Vue re-renders the button as disabled, so the
610
+ // second one reaches the handler and only its guard stops it.
611
+ test('Pay clicked twice before the first request answers sends one request', async () => {
612
+ await useLocale('de-DE');
613
+
614
+ const wrapper = mountForm(consumerProps.Payment);
615
+
616
+ await completeForm(wrapper);
617
+
618
+ let failIntent: (error: AxiosError) => void = () => undefined;
619
+
620
+ createStripePaymentIntent.mockReturnValueOnce(
621
+ new Promise((_resolve, reject) => {
622
+ failIntent = reject;
623
+ }),
624
+ );
625
+ await Promise.all([
626
+ payButton(wrapper).trigger('click'),
627
+ payButton(wrapper).trigger('click'),
628
+ ]);
629
+
630
+ expect(createStripePaymentIntent).toHaveBeenCalledTimes(1);
631
+ expect(console.error).toHaveBeenCalledWith(
632
+ 'onSubmitPaymentClick: invalid operation',
633
+ );
634
+
635
+ failIntent(httpError(500));
636
+ await flushPromises();
637
+
638
+ expect(createStripePaymentIntent).toHaveBeenCalledTimes(1);
639
+ expect(formErrors(wrapper)).toEqual([
640
+ stringsFor('de-DE').paymentIntentErrorMessage,
641
+ ]);
642
+ });
643
+ });
644
+ });
@@ -1,6 +1,9 @@
1
1
  {
2
2
  "requestPaymentStripe" : {
3
3
  "cardNumber" : "Kartennummer",
4
+ "cardNumberPlaceholder" : "Kreditkartennummer",
5
+ "cardExpiryPlaceholder" : "Ablaufdatum",
6
+ "cardCvcPlaceholder" : "Prüfnummer",
4
7
  "emailAddress" : "E-Mail-Adresse",
5
8
  "emailAddressPlaceholder" : "E-Mail-Adresse",
6
9
  "proceedToPayment" : "Weiter zur Zahlung {{CURRENCY_SYMBOL}}{{AMOUNT}} ({{CURRENCY_CODE}})",
@@ -9,6 +12,7 @@
9
12
  "termsLinkText" : "Nutzungsbedingungen von BrandCrowd",
10
13
  "termsDisclaimer" : "Die Zahlung wird an den Eigentümer der Webseite geleistet. BrandCrowd ist für diese Transaktion nicht haftbar.",
11
14
  "poweredBy" : "Powered by",
12
- "invalidEmailMessage" : "Bitte geben Sie eine gültige E-Mail-Adresse ein"
15
+ "invalidEmailMessage" : "Bitte geben Sie eine gültige E-Mail-Adresse ein",
16
+ "paymentIntentErrorMessage" : "Die Zahlung konnte nicht erstellt werden. Bitte versuchen Sie es erneut."
13
17
  }
14
18
  }
@@ -1,6 +1,9 @@
1
1
  {
2
2
  "requestPaymentStripe" : {
3
3
  "cardNumber" : "Número de tarjeta",
4
+ "cardNumberPlaceholder" : "Número de tarjeta de crédito",
5
+ "cardExpiryPlaceholder" : "Caducidad",
6
+ "cardCvcPlaceholder" : "CVV",
4
7
  "emailAddress" : "Dirección de correo electrónico",
5
8
  "emailAddressPlaceholder" : "Dirección de correo electrónico",
6
9
  "proceedToPayment" : "Procede al pago {{CURRENCY_SYMBOL}}{{AMOUNT}} ({{CURRENCY_CODE}})",
@@ -9,6 +12,7 @@
9
12
  "termsLinkText" : "Términos y condiciones de BrandCrowd",
10
13
  "termsDisclaimer" : "El pago se hará al propietario de la página web. BrandCrowd no es responsable de esta transacción.",
11
14
  "poweredBy" : "Desarrollado por",
12
- "invalidEmailMessage" : "Introduce una dirección de correo electrónico válida"
15
+ "invalidEmailMessage" : "Introduce una dirección de correo electrónico válida",
16
+ "paymentIntentErrorMessage" : "No se ha podido crear el pago. Inténtalo de nuevo."
13
17
  }
14
18
  }
@@ -1,6 +1,9 @@
1
1
  {
2
2
  "requestPaymentStripe" : {
3
3
  "cardNumber" : "Numéro de carte",
4
+ "cardNumberPlaceholder" : "Numéro de carte de crédit",
5
+ "cardExpiryPlaceholder" : "Expiration",
6
+ "cardCvcPlaceholder" : "CVV",
4
7
  "emailAddress" : "Adresse courriel",
5
8
  "emailAddressPlaceholder" : "Adresse courriel",
6
9
  "proceedToPayment" : "Procédez au paiement {{CURRENCY_SYMBOL}}{{AMOUNT}} ({{CURRENCY_CODE}})",
@@ -9,6 +12,7 @@
9
12
  "termsLinkText" : "Conditions générales de BrandCrowd",
10
13
  "termsDisclaimer" : "Le paiement sera envoyé au propriétaire de la page Web. BrandCrowd n'est pas responsable de cette transaction.",
11
14
  "poweredBy" : "Alimenté par",
12
- "invalidEmailMessage" : "Veuillez entrer une adresse e-mail valide"
15
+ "invalidEmailMessage" : "Veuillez entrer une adresse e-mail valide",
16
+ "paymentIntentErrorMessage" : "Impossible de créer le paiement. Veuillez réessayer."
13
17
  }
14
18
  }