diff --git a/package.json b/package.json index 4b6112a..faa35f1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "processout.js", - "version": "1.9.10", + "version": "1.9.11", "description": "ProcessOut.js is a JavaScript library for ProcessOut's payment processing API.", "scripts": { "build:processout": "tsc -p src/processout && uglifyjs --compress --keep-fnames --ie8 dist/processout.js -o dist/processout.js", diff --git a/src/dynamic-checkout/config/payment-config.ts b/src/dynamic-checkout/config/payment-config.ts index fa93987..6131ffe 100644 --- a/src/dynamic-checkout/config/payment-config.ts +++ b/src/dynamic-checkout/config/payment-config.ts @@ -70,6 +70,18 @@ module ProcessOut { this.invoiceDetails = invoiceDetails } + // Tokenize-only is exposed per payment method by the API, but it describes the whole + // checkout: the shopper is verifying a card, not paying for the invoice. + public isTokenizeOnly(): boolean { + if (!this.invoiceDetails || !this.invoiceDetails.payment_methods) { + return false + } + + return this.invoiceDetails.payment_methods.some( + paymentMethod => paymentMethod.card && paymentMethod.card.tokenize_only, + ) + } + public getAdditionalDataForGateway(gatewayName: string): Record { return this.additionalData[gatewayName] || {} } diff --git a/src/dynamic-checkout/dynamic-checkout.ts b/src/dynamic-checkout/dynamic-checkout.ts index 2d4cf89..c81807a 100644 --- a/src/dynamic-checkout/dynamic-checkout.ts +++ b/src/dynamic-checkout/dynamic-checkout.ts @@ -94,7 +94,7 @@ module ProcessOut { new DynamicCheckoutPaymentErrorView( this.processOutInstance, this.paymentConfig, - Translations.getText("payment-error-generic-message", this.paymentConfig.locale), + getStatusMessage("payment-error-generic-message", this.paymentConfig), ).element, ) @@ -106,12 +106,16 @@ module ProcessOut { ) } + // Set before the status check below so the error view and event know whether this + // checkout is a tokenize-only (card verification) one. + this.paymentConfig.setInvoiceDetails(data.invoice) + if (data.invoice.transaction.status !== "waiting") { this.loadView( new DynamicCheckoutPaymentErrorView( this.processOutInstance, this.paymentConfig, - Translations.getText("payment-error-generic-message", this.paymentConfig.locale), + getStatusMessage("payment-error-generic-message", this.paymentConfig), ).element, ) @@ -125,11 +129,12 @@ module ProcessOut { undefined, undefined, data.invoice.return_url || null, + undefined, + undefined, + this.paymentConfig.isTokenizeOnly(), ) } - this.paymentConfig.setInvoiceDetails(data.invoice) - this.loadDynamicCheckoutView() } diff --git a/src/dynamic-checkout/locales/ar.ts b/src/dynamic-checkout/locales/ar.ts index ed6e3dc..bd845d7 100644 --- a/src/dynamic-checkout/locales/ar.ts +++ b/src/dynamic-checkout/locales/ar.ts @@ -24,6 +24,13 @@ module ProcessOut { "payment-info-message": "نقوم بمعالجة عملية الدفع الخاصة بك. يمكنك الآن إغلاق هذه النافذة.", "payment-pending-message": "عملية الدفع الخاصة بك قيد الانتظار.", "payment-error-generic-message": "لم نتمكن من معالجة عملية الدفع الخاصة بك.", + "verification-success-message": "اكتملت عملية التحقق هذه.", + "verification-cancelled-message": "تم إلغاء عملية التحقق.", + "verification-info-message": + "نقوم بمعالجة عملية التحقق الخاصة بك. يمكنك الآن إغلاق هذه النافذة.", + "verification-pending-message": "عملية التحقق الخاصة بك قيد الانتظار.", + "verification-error-generic-message": "لم نتمكن من التحقق من بطاقتك.", + "processing-verification-label": "جارٍ معالجة التحقق", "street1-label": "العنوان السطر 1", "street2-label": "العنوان السطر 2", "city-label": "المدينة", diff --git a/src/dynamic-checkout/locales/de.ts b/src/dynamic-checkout/locales/de.ts index 1d6b0ad..af46c68 100644 --- a/src/dynamic-checkout/locales/de.ts +++ b/src/dynamic-checkout/locales/de.ts @@ -25,6 +25,13 @@ module ProcessOut { "Wir verarbeiten Ihre Zahlung. Sie können dieses Fenster jetzt schließen.", "payment-pending-message": "Ihre Zahlung steht noch aus.", "payment-error-generic-message": "Wir konnten Ihre Zahlung nicht verarbeiten.", + "verification-success-message": "Diese Verifizierung wurde abgeschlossen.", + "verification-cancelled-message": "Die Verifizierung wurde abgebrochen.", + "verification-info-message": + "Wir verarbeiten Ihre Verifizierung. Sie können dieses Fenster jetzt schließen.", + "verification-pending-message": "Ihre Verifizierung steht noch aus.", + "verification-error-generic-message": "Wir konnten Ihre Karte nicht verifizieren.", + "processing-verification-label": "Verifizierung wird verarbeitet", "street1-label": "Adresszeile 1", "street2-label": "Adresszeile 2", "city-label": "Stadt", diff --git a/src/dynamic-checkout/locales/en.ts b/src/dynamic-checkout/locales/en.ts index 227bf9f..7b5bcbc 100644 --- a/src/dynamic-checkout/locales/en.ts +++ b/src/dynamic-checkout/locales/en.ts @@ -24,6 +24,13 @@ module ProcessOut { "payment-info-message": "We are processing your payment. You can now close this window.", "payment-pending-message": "Your payment is pending.", "payment-error-generic-message": "We were unable to process your payment.", + "verification-success-message": "This verification is completed.", + "verification-cancelled-message": "Verification has been cancelled.", + "verification-info-message": + "We are processing your verification. You can now close this window.", + "verification-pending-message": "Your verification is pending.", + "verification-error-generic-message": "We were unable to verify your card.", + "processing-verification-label": "Processing verification", "street1-label": "Address line 1", "street2-label": "Address line 2", "city-label": "City", diff --git a/src/dynamic-checkout/locales/es.ts b/src/dynamic-checkout/locales/es.ts index 3bcbe2a..da26925 100644 --- a/src/dynamic-checkout/locales/es.ts +++ b/src/dynamic-checkout/locales/es.ts @@ -24,6 +24,12 @@ module ProcessOut { "payment-info-message": "Estamos procesando su pago. Puede cerrar esta ventana.", "payment-pending-message": "Su pago está pendiente.", "payment-error-generic-message": "No pudimos procesar su pago.", + "verification-success-message": "Esta verificación se ha completado.", + "verification-cancelled-message": "La verificación ha sido cancelada.", + "verification-info-message": "Estamos procesando su verificación. Puede cerrar esta ventana.", + "verification-pending-message": "Su verificación está pendiente.", + "verification-error-generic-message": "No pudimos verificar su tarjeta.", + "processing-verification-label": "Procesando verificación", "street1-label": "Dirección, línea 1", "street2-label": "Dirección, línea 2", "city-label": "Ciudad", diff --git a/src/dynamic-checkout/locales/fi.ts b/src/dynamic-checkout/locales/fi.ts index b6f9ff5..31f4118 100644 --- a/src/dynamic-checkout/locales/fi.ts +++ b/src/dynamic-checkout/locales/fi.ts @@ -24,6 +24,12 @@ module ProcessOut { "payment-info-message": "Käsittelemme maksuasi. Voit nyt sulkea tämän ikkunan.", "payment-pending-message": "Maksusi on käsittelyssä.", "payment-error-generic-message": "Maksuasi ei voitu käsitellä.", + "verification-success-message": "Tämä vahvistus on suoritettu.", + "verification-cancelled-message": "Vahvistus on peruutettu.", + "verification-info-message": "Käsittelemme vahvistustasi. Voit nyt sulkea tämän ikkunan.", + "verification-pending-message": "Vahvistuksesi on käsittelyssä.", + "verification-error-generic-message": "Korttiasi ei voitu vahvistaa.", + "processing-verification-label": "Käsitellään vahvistusta", "street1-label": "Osoiterivi 1", "street2-label": "Osoiterivi 2", "city-label": "Kaupunki", diff --git a/src/dynamic-checkout/locales/fr.ts b/src/dynamic-checkout/locales/fr.ts index 4b71e72..e475208 100644 --- a/src/dynamic-checkout/locales/fr.ts +++ b/src/dynamic-checkout/locales/fr.ts @@ -25,6 +25,13 @@ module ProcessOut { "Nous travaillons sur votre paiement. Vous pouvez maintenant fermer cette fenêtre.", "payment-pending-message": "Votre paiement est en attente.", "payment-error-generic-message": "Nous n'avons pas pu traiter votre paiement.", + "verification-success-message": "Cette vérification a été effectuée avec succès.", + "verification-cancelled-message": "La vérification a été annulée.", + "verification-info-message": + "Nous travaillons sur votre vérification. Vous pouvez maintenant fermer cette fenêtre.", + "verification-pending-message": "Votre vérification est en attente.", + "verification-error-generic-message": "Nous n'avons pas pu vérifier votre carte.", + "processing-verification-label": "Traitement de la vérification", "street1-label": "Adresse, ligne 1", "street2-label": "Adresse, ligne 2", "city-label": "Ville", diff --git a/src/dynamic-checkout/locales/it.ts b/src/dynamic-checkout/locales/it.ts index 15127a1..58922c7 100644 --- a/src/dynamic-checkout/locales/it.ts +++ b/src/dynamic-checkout/locales/it.ts @@ -25,6 +25,13 @@ module ProcessOut { "Stiamo elaborando il tuo pagamento. Ora puoi chiudere questa finestra.", "payment-pending-message": "Il tuo pagamento è in sospeso.", "payment-error-generic-message": "Non è stato possibile elaborare il pagamento.", + "verification-success-message": "La verifica è stata completata.", + "verification-cancelled-message": "La verifica è stata annullata.", + "verification-info-message": + "Stiamo elaborando la tua verifica. Ora puoi chiudere questa finestra.", + "verification-pending-message": "La tua verifica è in sospeso.", + "verification-error-generic-message": "Non è stato possibile verificare la carta.", + "processing-verification-label": "Elaborazione della verifica", "street1-label": "Indirizzo riga 1", "street2-label": "Indirizzo riga 2", "city-label": "Città", diff --git a/src/dynamic-checkout/locales/ja.ts b/src/dynamic-checkout/locales/ja.ts index c0202ec..09bb5cc 100644 --- a/src/dynamic-checkout/locales/ja.ts +++ b/src/dynamic-checkout/locales/ja.ts @@ -25,6 +25,13 @@ module ProcessOut { "お支払いを処理しています。このウィンドウを閉じていただいて構いません。", "payment-pending-message": "お支払いは保留中です。", "payment-error-generic-message": "お支払いを処理できませんでした。", + "verification-success-message": "確認が完了しました。", + "verification-cancelled-message": "確認がキャンセルされました。", + "verification-info-message": + "確認を処理しています。このウィンドウを閉じていただいて構いません。", + "verification-pending-message": "確認は保留中です。", + "verification-error-generic-message": "カードを確認できませんでした。", + "processing-verification-label": "確認を処理中", "street1-label": "住所1", "street2-label": "住所2", "city-label": "市区町村", diff --git a/src/dynamic-checkout/locales/ko.ts b/src/dynamic-checkout/locales/ko.ts index 2ee7aea..e819d52 100644 --- a/src/dynamic-checkout/locales/ko.ts +++ b/src/dynamic-checkout/locales/ko.ts @@ -24,6 +24,12 @@ module ProcessOut { "payment-info-message": "결제를 처리하고 있습니다. 이 창을 닫으셔도 됩니다.", "payment-pending-message": "결제가 대기 중입니다.", "payment-error-generic-message": "결제를 처리할 수 없습니다.", + "verification-success-message": "인증이 완료되었습니다.", + "verification-cancelled-message": "인증이 취소되었습니다.", + "verification-info-message": "인증을 처리하고 있습니다. 이 창을 닫으셔도 됩니다.", + "verification-pending-message": "인증이 대기 중입니다.", + "verification-error-generic-message": "카드를 인증할 수 없습니다.", + "processing-verification-label": "인증 처리 중", "street1-label": "주소 1", "street2-label": "주소 2", "city-label": "시/군/구", diff --git a/src/dynamic-checkout/locales/nb.ts b/src/dynamic-checkout/locales/nb.ts index fb3c611..b0b2af3 100644 --- a/src/dynamic-checkout/locales/nb.ts +++ b/src/dynamic-checkout/locales/nb.ts @@ -24,6 +24,12 @@ module ProcessOut { "payment-info-message": "Vi behandler betalingen din. Du kan nå lukke dette vinduet.", "payment-pending-message": "Betalingen din er under behandling.", "payment-error-generic-message": "Vi kunne ikke behandle betalingen din.", + "verification-success-message": "Denne verifiseringen er fullført.", + "verification-cancelled-message": "Verifiseringen er kansellert.", + "verification-info-message": "Vi behandler verifiseringen din. Du kan nå lukke dette vinduet.", + "verification-pending-message": "Verifiseringen din er under behandling.", + "verification-error-generic-message": "Vi kunne ikke verifisere kortet ditt.", + "processing-verification-label": "Behandler verifisering", "street1-label": "Adresselinje 1", "street2-label": "Adresselinje 2", "city-label": "By", diff --git a/src/dynamic-checkout/locales/pl.ts b/src/dynamic-checkout/locales/pl.ts index f64efa3..b25bcc7 100644 --- a/src/dynamic-checkout/locales/pl.ts +++ b/src/dynamic-checkout/locales/pl.ts @@ -24,6 +24,12 @@ module ProcessOut { "payment-info-message": "Przetwarzamy Twoją płatność. Możesz teraz zamknąć tę okno.", "payment-pending-message": "Twoja płatność oczekuje na realizację.", "payment-error-generic-message": "Nie udało nam się przetworzyć Twojej płatności.", + "verification-success-message": "Weryfikacja została zakończona pomyślnie.", + "verification-cancelled-message": "Weryfikacja została anulowana.", + "verification-info-message": "Przetwarzamy Twoją weryfikację. Możesz teraz zamknąć to okno.", + "verification-pending-message": "Twoja weryfikacja jest w toku.", + "verification-error-generic-message": "Nie udało nam się zweryfikować Twojej karty.", + "processing-verification-label": "Przetwarzanie weryfikacji", "street1-label": "Adres, linia 1", "street2-label": "Adres, linia 2", "city-label": "Miasto", diff --git a/src/dynamic-checkout/locales/pt.ts b/src/dynamic-checkout/locales/pt.ts index 0cf2ade..6d33cfe 100644 --- a/src/dynamic-checkout/locales/pt.ts +++ b/src/dynamic-checkout/locales/pt.ts @@ -21,10 +21,16 @@ module ProcessOut { "other-payment-methods-header": "Outros métodos de pagamento", "select-payment-method-label": "Selecionar método de pagamento", "payment-success-message": "Este pagamento foi concluído.", - "payment-info-message": - "Estamos a processar o seu pagamento. Pode agora fechar esta janela.", + "payment-info-message": "Estamos a processar o seu pagamento. Pode agora fechar esta janela.", "payment-pending-message": "O seu pagamento está pendente.", "payment-error-generic-message": "Não foi possível processar o seu pagamento.", + "verification-success-message": "Esta verificação foi concluída.", + "verification-cancelled-message": "A verificação foi cancelada.", + "verification-info-message": + "Estamos a processar a sua verificação. Pode agora fechar esta janela.", + "verification-pending-message": "A sua verificação está pendente.", + "verification-error-generic-message": "Não foi possível verificar o seu cartão.", + "processing-verification-label": "A processar verificação", "street1-label": "Morada, linha 1", "street2-label": "Morada, linha 2", "city-label": "Cidade", diff --git a/src/dynamic-checkout/locales/ta.ts b/src/dynamic-checkout/locales/ta.ts index 6990a76..cfe8e93 100644 --- a/src/dynamic-checkout/locales/ta.ts +++ b/src/dynamic-checkout/locales/ta.ts @@ -24,6 +24,13 @@ module ProcessOut { "payment-info-message": "உங்கள் கட்டணத்தைச் செயலாக்குகிறோம். இப்போது இந்தச் சாளரத்தை மூடலாம்.", "payment-pending-message": "உங்கள் கட்டணம் நிலுவையில் உள்ளது.", "payment-error-generic-message": "உங்கள் கட்டணத்தைச் செயலாக்க முடியவில்லை.", + "verification-success-message": "இந்தச் சரிபார்ப்பு முடிந்தது.", + "verification-cancelled-message": "சரிபார்ப்பு ரத்து செய்யப்பட்டது.", + "verification-info-message": + "உங்கள் சரிபார்ப்பைச் செயலாக்குகிறோம். இப்போது இந்தச் சாளரத்தை மூடலாம்.", + "verification-pending-message": "உங்கள் சரிபார்ப்பு நிலுவையில் உள்ளது.", + "verification-error-generic-message": "உங்கள் அட்டையைச் சரிபார்க்க முடியவில்லை.", + "processing-verification-label": "சரிபார்ப்பு செயலாக்கப்படுகிறது", "street1-label": "முகவரி வரி 1", "street2-label": "முகவரி வரி 2", "city-label": "நகரம்", diff --git a/src/dynamic-checkout/locales/vi.ts b/src/dynamic-checkout/locales/vi.ts index aab52d5..992f11d 100644 --- a/src/dynamic-checkout/locales/vi.ts +++ b/src/dynamic-checkout/locales/vi.ts @@ -25,6 +25,13 @@ module ProcessOut { "Chúng tôi đang xử lý thanh toán của bạn. Bạn có thể đóng cửa sổ này ngay bây giờ.", "payment-pending-message": "Thanh toán của bạn đang chờ xử lý.", "payment-error-generic-message": "Chúng tôi không thể xử lý thanh toán của bạn.", + "verification-success-message": "Xác minh này đã hoàn tất.", + "verification-cancelled-message": "Xác minh đã bị hủy.", + "verification-info-message": + "Chúng tôi đang xử lý xác minh của bạn. Bạn có thể đóng cửa sổ này ngay bây giờ.", + "verification-pending-message": "Xác minh của bạn đang chờ xử lý.", + "verification-error-generic-message": "Chúng tôi không thể xác minh thẻ của bạn.", + "processing-verification-label": "Đang xử lý xác minh", "street1-label": "Địa chỉ dòng 1", "street2-label": "Địa chỉ dòng 2", "city-label": "Thành phố", diff --git a/src/dynamic-checkout/payment-methods/card.ts b/src/dynamic-checkout/payment-methods/card.ts index 7dfb18c..8afe17b 100644 --- a/src/dynamic-checkout/payment-methods/card.ts +++ b/src/dynamic-checkout/payment-methods/card.ts @@ -93,12 +93,15 @@ module ProcessOut { this.setButtonLoading() this.tokenizedCardId = undefined - DynamicCheckoutEventsUtils.dispatchPaymentSubmittedEvent({ - payment_method_name: "card", - payment_method_display_name: this.paymentMethodDisplayName, - invoice_id: this.paymentConfig.invoiceId, - return_url: this.paymentConfig.invoiceDetails.return_url || null, - }) + DynamicCheckoutEventsUtils.dispatchPaymentSubmittedEvent( + { + payment_method_name: "card", + payment_method_display_name: this.paymentMethodDisplayName, + invoice_id: this.paymentConfig.invoiceId, + return_url: this.paymentConfig.invoiceDetails.return_url || null, + }, + this.paymentConfig.isTokenizeOnly(), + ) this.processOutInstance.tokenize( cardForm, @@ -118,6 +121,7 @@ module ProcessOut { this.paymentConfig.invoiceDetails.return_url || null, undefined, this.paymentMethodDisplayName, + this.paymentConfig.isTokenizeOnly(), ), ) } @@ -174,23 +178,28 @@ module ProcessOut { this.paymentConfig.invoiceDetails.return_url || null, undefined, this.paymentMethodDisplayName, + this.paymentConfig.isTokenizeOnly(), ) } private handleCardPaymentSuccess(invoiceId: string, data?: any) { - DynamicCheckoutEventsUtils.dispatchPaymentSuccessEvent({ - invoice_id: invoiceId, - return_url: this.paymentConfig.invoiceDetails.return_url || null, - payment_method_name: "card", - payment_method_display_name: this.paymentMethodDisplayName, - ...(this.tokenizedCardId && { card_id: this.tokenizedCardId }), - customer_token_id: data?.customer_token_id, - ...DynamicCheckoutEventsUtils.getPaymentStatusEventDetail(data), - }) + DynamicCheckoutEventsUtils.dispatchPaymentSuccessEvent( + { + invoice_id: invoiceId, + return_url: this.paymentConfig.invoiceDetails.return_url || null, + payment_method_name: "card", + payment_method_display_name: this.paymentMethodDisplayName, + ...(this.tokenizedCardId && { card_id: this.tokenizedCardId }), + customer_token_id: data?.customer_token_id, + ...DynamicCheckoutEventsUtils.getPaymentStatusEventDetail(data), + }, + this.paymentConfig.isTokenizeOnly(), + ) if (this.paymentConfig.showStatusMessage) { this.resetContainerHtml().appendChild( - new DynamicCheckoutPaymentSuccessView(this.processOutInstance, this.paymentConfig).element, + new DynamicCheckoutPaymentSuccessView(this.processOutInstance, this.paymentConfig) + .element, ) } else if ( !this.paymentConfig.showStatusMessage && @@ -205,18 +214,22 @@ module ProcessOut { private handleCardPaymentPending(invoiceId: string, _reason: string | null, data?: any) { if (this.paymentConfig.showStatusMessage) { this.resetContainerHtml().appendChild( - new DynamicCheckoutPaymentPendingView(this.processOutInstance, this.paymentConfig).element, + new DynamicCheckoutPaymentPendingView(this.processOutInstance, this.paymentConfig) + .element, ) } - DynamicCheckoutEventsUtils.dispatchPaymentPendingEvent({ - payment_method_name: "card", - payment_method_display_name: this.paymentMethodDisplayName, - invoice_id: invoiceId, - return_url: this.paymentConfig.invoiceDetails.return_url || null, - ...(this.tokenizedCardId && { card_id: this.tokenizedCardId }), - ...DynamicCheckoutEventsUtils.getPaymentStatusEventDetail(data), - }) + DynamicCheckoutEventsUtils.dispatchPaymentPendingEvent( + { + payment_method_name: "card", + payment_method_display_name: this.paymentMethodDisplayName, + invoice_id: invoiceId, + return_url: this.paymentConfig.invoiceDetails.return_url || null, + ...(this.tokenizedCardId && { card_id: this.tokenizedCardId }), + ...DynamicCheckoutEventsUtils.getPaymentStatusEventDetail(data), + }, + this.paymentConfig.isTokenizeOnly(), + ) } private handleCardPaymentError(error) { @@ -226,16 +239,19 @@ module ProcessOut { .element, ) - DynamicCheckoutEventsUtils.dispatchPaymentCancelledEvent({ - payment_method_name: "card", - payment_method_display_name: this.paymentMethodDisplayName, - invoice_id: this.paymentConfig.invoiceId, - return_url: this.paymentConfig.invoiceDetails.return_url || null, - // Card 3DS challenges run in an iframe overlay (no tab/window). - // The SDK can still report iframe closes as reason "tab_closed", but we treat this as an in-overlay cancel. - // For event reporting we therefore force `tab_closed: false` below. - tab_closed: false, - }) + DynamicCheckoutEventsUtils.dispatchPaymentCancelledEvent( + { + payment_method_name: "card", + payment_method_display_name: this.paymentMethodDisplayName, + invoice_id: this.paymentConfig.invoiceId, + return_url: this.paymentConfig.invoiceDetails.return_url || null, + // Card 3DS challenges run in an iframe overlay (no tab/window). + // The SDK can still report iframe closes as reason "tab_closed", but we treat this as an in-overlay cancel. + // For event reporting we therefore force `tab_closed: false` below. + tab_closed: false, + }, + this.paymentConfig.isTokenizeOnly(), + ) return } @@ -261,6 +277,7 @@ module ProcessOut { this.paymentConfig.invoiceDetails.return_url || null, undefined, this.paymentMethodDisplayName, + this.paymentConfig.isTokenizeOnly(), ) } @@ -724,7 +741,7 @@ module ProcessOut { payButton.textContent = "" payButton.setAttribute( "aria-label", - Translations.getText("processing-payment-label", this.paymentConfig.locale), + getStatusMessage("processing-payment-label", this.paymentConfig), ) const spinner = HTMLElements.createElement({ diff --git a/src/dynamic-checkout/references.ts b/src/dynamic-checkout/references.ts index 24f1b84..8283dd5 100644 --- a/src/dynamic-checkout/references.ts +++ b/src/dynamic-checkout/references.ts @@ -40,6 +40,7 @@ /// /// /// +/// /// /// /// diff --git a/src/dynamic-checkout/utils/events.ts b/src/dynamic-checkout/utils/events.ts index 8d70a01..faa85c7 100644 --- a/src/dynamic-checkout/utils/events.ts +++ b/src/dynamic-checkout/utils/events.ts @@ -19,6 +19,13 @@ module ProcessOut { DELETE_PAYMENT_METHOD_ERROR: "processout_dynamic_checkout_delete_payment_method_error", PAYMENT_SUBMITTED: "processout_dynamic_checkout_payment_submitted", PAYMENT_PENDING: "processout_dynamic_checkout_payment_pending", + // Tokenize-only checkouts verify a card instead of taking a payment, so they emit + // this parallel set of events rather than the PAYMENT_* ones above. + CARD_VERIFICATION_SUBMITTED: "processout_dynamic_checkout_card_verification_submitted", + CARD_VERIFICATION_SUCCESS: "processout_dynamic_checkout_card_verification_success", + CARD_VERIFICATION_PENDING: "processout_dynamic_checkout_card_verification_pending", + CARD_VERIFICATION_CANCELLED: "processout_dynamic_checkout_card_verification_cancelled", + CARD_VERIFICATION_ERROR: "processout_dynamic_checkout_card_verification_error", } interface DynamicCheckoutEventDetail { @@ -104,6 +111,7 @@ module ProcessOut { returnUrl?: string | null, customerTokenId?: string, paymentMethodDisplayName?: string, + isCardVerification?: boolean, ) { const normalizedError = DynamicCheckoutEventsUtils.normalizePaymentError( invoiceId, @@ -121,15 +129,22 @@ module ProcessOut { } const event = DynamicCheckoutEventsUtils.createEvent( - DYNAMIC_CHECKOUT_EVENTS.PAYMENT_ERROR, + isCardVerification + ? DYNAMIC_CHECKOUT_EVENTS.CARD_VERIFICATION_ERROR + : DYNAMIC_CHECKOUT_EVENTS.PAYMENT_ERROR, normalizedError, ) return window.dispatchEvent(event) } - static dispatchPaymentSuccessEvent(response: DynamicCheckoutPaymentSuccessEventDetail) { + static dispatchPaymentSuccessEvent( + response: DynamicCheckoutPaymentSuccessEventDetail, + isCardVerification?: boolean, + ) { const event = DynamicCheckoutEventsUtils.createEvent( - DYNAMIC_CHECKOUT_EVENTS.PAYMENT_SUCCESS, + isCardVerification + ? DYNAMIC_CHECKOUT_EVENTS.CARD_VERIFICATION_SUCCESS + : DYNAMIC_CHECKOUT_EVENTS.PAYMENT_SUCCESS, { ...response, payment_method_name: response.payment_method_name || null, @@ -258,52 +273,67 @@ module ProcessOut { return window.dispatchEvent(event) } - static dispatchPaymentSubmittedEvent(details: { - payment_method_name: string - payment_method_display_name: string - invoice_id: string - return_url: string | null - customer_token_id?: string - authorized?: boolean - captured?: boolean - }) { + static dispatchPaymentSubmittedEvent( + details: { + payment_method_name: string + payment_method_display_name: string + invoice_id: string + return_url: string | null + customer_token_id?: string + authorized?: boolean + captured?: boolean + }, + isCardVerification?: boolean, + ) { const event = DynamicCheckoutEventsUtils.createEvent( - DYNAMIC_CHECKOUT_EVENTS.PAYMENT_SUBMITTED, + isCardVerification + ? DYNAMIC_CHECKOUT_EVENTS.CARD_VERIFICATION_SUBMITTED + : DYNAMIC_CHECKOUT_EVENTS.PAYMENT_SUBMITTED, details, ) return window.dispatchEvent(event) } - static dispatchPaymentCancelledEvent(details: { - payment_method_name: string - payment_method_display_name: string - invoice_id: string - return_url: string | null - tab_closed?: boolean - authorized?: boolean - captured?: boolean - }) { + static dispatchPaymentCancelledEvent( + details: { + payment_method_name: string + payment_method_display_name: string + invoice_id: string + return_url: string | null + tab_closed?: boolean + authorized?: boolean + captured?: boolean + }, + isCardVerification?: boolean, + ) { const event = DynamicCheckoutEventsUtils.createEvent( - DYNAMIC_CHECKOUT_EVENTS.PAYMENT_CANCELLED, + isCardVerification + ? DYNAMIC_CHECKOUT_EVENTS.CARD_VERIFICATION_CANCELLED + : DYNAMIC_CHECKOUT_EVENTS.PAYMENT_CANCELLED, details, ) return window.dispatchEvent(event) } - static dispatchPaymentPendingEvent(details: { - payment_method_name: string - payment_method_display_name: string - invoice_id: string - return_url: string | null - card_id?: string - customer_token_id?: string - authorized?: boolean - captured?: boolean - }) { + static dispatchPaymentPendingEvent( + details: { + payment_method_name: string + payment_method_display_name: string + invoice_id: string + return_url: string | null + card_id?: string + customer_token_id?: string + authorized?: boolean + captured?: boolean + }, + isCardVerification?: boolean, + ) { const event = DynamicCheckoutEventsUtils.createEvent( - DYNAMIC_CHECKOUT_EVENTS.PAYMENT_PENDING, + isCardVerification + ? DYNAMIC_CHECKOUT_EVENTS.CARD_VERIFICATION_PENDING + : DYNAMIC_CHECKOUT_EVENTS.PAYMENT_PENDING, { ...details, ...DynamicCheckoutEventsUtils.getPaymentStatusEventDetail(details), diff --git a/src/dynamic-checkout/utils/status-messages.ts b/src/dynamic-checkout/utils/status-messages.ts new file mode 100644 index 0000000..c5c9252 --- /dev/null +++ b/src/dynamic-checkout/utils/status-messages.ts @@ -0,0 +1,21 @@ +/// + +module ProcessOut { + // Tokenize-only checkouts show the same status screens as a payment, but nothing is + // charged - so every message that mentions a payment has a verification counterpart. + const VERIFICATION_MESSAGE_KEYS = { + "payment-success-message": "verification-success-message", + "payment-cancelled-message": "verification-cancelled-message", + "payment-error-generic-message": "verification-error-generic-message", + "payment-pending-message": "verification-pending-message", + "payment-info-message": "verification-info-message", + "processing-payment-label": "processing-verification-label", + } + + export function getStatusMessage(key: string, paymentConfig: DynamicCheckoutPaymentConfig) { + const verificationKey = VERIFICATION_MESSAGE_KEYS[key] + const resolvedKey = verificationKey && paymentConfig.isTokenizeOnly() ? verificationKey : key + + return Translations.getText(resolvedKey, paymentConfig.locale) + } +} diff --git a/src/dynamic-checkout/views/payment-cancelled.ts b/src/dynamic-checkout/views/payment-cancelled.ts index 66547ec..de1de7d 100644 --- a/src/dynamic-checkout/views/payment-cancelled.ts +++ b/src/dynamic-checkout/views/payment-cancelled.ts @@ -24,7 +24,7 @@ module ProcessOut { { tagName: "div", classNames: ["dco-card-payment-error-text"], - textContent: Translations.getText("payment-cancelled-message", paymentConfig.locale), + textContent: getStatusMessage("payment-cancelled-message", paymentConfig), }, ]) diff --git a/src/dynamic-checkout/views/payment-info.ts b/src/dynamic-checkout/views/payment-info.ts index 2b64aba..d12ac34 100644 --- a/src/dynamic-checkout/views/payment-info.ts +++ b/src/dynamic-checkout/views/payment-info.ts @@ -16,7 +16,7 @@ module ProcessOut { { tagName: "p", classNames: ["dco-card-payment-success-text"], - textContent: Translations.getText("payment-info-message", paymentConfig.locale), + textContent: getStatusMessage("payment-info-message", paymentConfig), }, ]) diff --git a/src/dynamic-checkout/views/payment-methods.ts b/src/dynamic-checkout/views/payment-methods.ts index 8455970..3000295 100644 --- a/src/dynamic-checkout/views/payment-methods.ts +++ b/src/dynamic-checkout/views/payment-methods.ts @@ -28,7 +28,7 @@ module ProcessOut { return new DynamicCheckoutPaymentErrorView( this.processOutInstance, this.paymentConfig, - Translations.getText("payment-error-generic-message", this.paymentConfig.locale), + getStatusMessage("payment-error-generic-message", this.paymentConfig), ).element as HTMLElement } @@ -377,7 +377,7 @@ module ProcessOut { } private getVisiblePaymentMethods() { - if (!this.paymentConfig.hideSavedPaymentMethods && !this.hasTokenizeOnlyCard()) { + if (!this.paymentConfig.hideSavedPaymentMethods && !this.paymentConfig.isTokenizeOnly()) { return this.paymentConfig.invoiceDetails.payment_methods } @@ -386,12 +386,6 @@ module ProcessOut { ) } - private hasTokenizeOnlyCard() { - return this.paymentConfig.invoiceDetails.payment_methods.some( - paymentMethod => paymentMethod.card && paymentMethod.card.tokenize_only, - ) - } - private isSavedPaymentMethod(paymentMethod: PaymentMethod) { return ( paymentMethod.type === "apm_customer_token" || paymentMethod.type === "card_customer_token" diff --git a/src/dynamic-checkout/views/payment-pending.ts b/src/dynamic-checkout/views/payment-pending.ts index 3965936..bf412d6 100644 --- a/src/dynamic-checkout/views/payment-pending.ts +++ b/src/dynamic-checkout/views/payment-pending.ts @@ -24,7 +24,7 @@ module ProcessOut { { tagName: "p", classNames: ["dco-card-payment-success-text"], - textContent: Translations.getText("payment-pending-message", paymentConfig.locale), + textContent: getStatusMessage("payment-pending-message", paymentConfig), }, ]) diff --git a/src/dynamic-checkout/views/payment-success.ts b/src/dynamic-checkout/views/payment-success.ts index 8318cc4..46284be 100644 --- a/src/dynamic-checkout/views/payment-success.ts +++ b/src/dynamic-checkout/views/payment-success.ts @@ -24,7 +24,7 @@ module ProcessOut { { tagName: "p", classNames: ["dco-card-payment-success-text"], - textContent: Translations.getText("payment-success-message", paymentConfig.locale), + textContent: getStatusMessage("payment-success-message", paymentConfig), }, ]) diff --git a/test/dynamic-checkout/tokenize-only.test.ts b/test/dynamic-checkout/tokenize-only.test.ts new file mode 100644 index 0000000..eefe468 --- /dev/null +++ b/test/dynamic-checkout/tokenize-only.test.ts @@ -0,0 +1,183 @@ +import { beforeEach, describe, expect, it } from "vitest" +import { CapturedEvent, LOCALES, loadDynamicCheckout } from "../support/loadNamespace" + +let ProcessOut: Record +let dispatchedEvents: CapturedEvent[] + +beforeEach(() => { + const loaded = loadDynamicCheckout() + ProcessOut = loaded.namespace + dispatchedEvents = loaded.dispatchedEvents +}) + +function createPaymentConfig(paymentMethods: any[] | null, locale = "en") { + const paymentConfig = new ProcessOut.DynamicCheckoutPaymentConfig({ + projectId: "proj_test", + invoiceId: "iv_test", + locale, + }) + + if (paymentMethods) { + paymentConfig.setInvoiceDetails({ payment_methods: paymentMethods }) + } + + return paymentConfig +} + +const tokenizeOnlyMethod = { type: "card", card: { tokenize_only: true } } +const regularCardMethod = { type: "card", card: { tokenize_only: false } } + +describe("DynamicCheckoutPaymentConfig.isTokenizeOnly", () => { + it("is false before the invoice has been fetched", () => { + expect(createPaymentConfig(null).isTokenizeOnly()).toBe(false) + }) + + it("is false for a regular card checkout", () => { + expect(createPaymentConfig([regularCardMethod]).isTokenizeOnly()).toBe(false) + }) + + it("is false when the card method predates the tokenize_only flag", () => { + expect(createPaymentConfig([{ type: "card", card: {} }]).isTokenizeOnly()).toBe(false) + }) + + it("ignores non-card payment methods", () => { + expect(createPaymentConfig([{ type: "apm", apm: {} }]).isTokenizeOnly()).toBe(false) + }) + + it("is true when any card method is tokenize-only", () => { + expect(createPaymentConfig([regularCardMethod, tokenizeOnlyMethod]).isTokenizeOnly()).toBe(true) + }) +}) + +describe("getStatusMessage", () => { + const mappedKeys = [ + "payment-success-message", + "payment-cancelled-message", + "payment-error-generic-message", + "payment-pending-message", + "payment-info-message", + "processing-payment-label", + ] + + it.each(LOCALES)("keeps the payment wording for a regular checkout in %s", locale => { + const paymentConfig = createPaymentConfig([regularCardMethod], locale) + + mappedKeys.forEach(key => { + expect(ProcessOut.getStatusMessage(key, paymentConfig)).toBe( + ProcessOut.Translations.getText(key, locale), + ) + }) + }) + + it.each(LOCALES)("uses translated verification wording in %s", locale => { + const paymentConfig = createPaymentConfig([tokenizeOnlyMethod], locale) + + mappedKeys.forEach(key => { + const message = ProcessOut.getStatusMessage(key, paymentConfig) + + // A missing locale key resolves to "" and would render a blank status screen. + expect(message, `${locale} is missing a verification message for ${key}`).not.toBe("") + expect(message).not.toBe(ProcessOut.Translations.getText(key, locale)) + }) + }) + + it("leaves keys without a verification counterpart untouched", () => { + const tokenizeOnlyConfig = createPaymentConfig([tokenizeOnlyMethod]) + + expect(ProcessOut.getStatusMessage("payment-error-message", tokenizeOnlyConfig)).toBe( + ProcessOut.Translations.getText("payment-error-message", "en"), + ) + }) + + it("resolves the English verification copy", () => { + const tokenizeOnlyConfig = createPaymentConfig([tokenizeOnlyMethod]) + + expect(ProcessOut.getStatusMessage("payment-success-message", tokenizeOnlyConfig)).toBe( + "This verification is completed.", + ) + expect(ProcessOut.getStatusMessage("payment-cancelled-message", tokenizeOnlyConfig)).toBe( + "Verification has been cancelled.", + ) + expect(ProcessOut.getStatusMessage("payment-error-generic-message", tokenizeOnlyConfig)).toBe( + "We were unable to verify your card.", + ) + }) +}) + +describe("DynamicCheckoutEventsUtils event names", () => { + const details = { + payment_method_name: "card", + payment_method_display_name: "Card", + invoice_id: "iv_test", + return_url: null, + } + + function lastEvent() { + return dispatchedEvents[dispatchedEvents.length - 1] + } + + const objectDispatchers = [ + ["dispatchPaymentSubmittedEvent", "submitted"], + ["dispatchPaymentSuccessEvent", "success"], + ["dispatchPaymentPendingEvent", "pending"], + ["dispatchPaymentCancelledEvent", "cancelled"], + ] as const + + it.each(objectDispatchers)("%s emits a payment event by default", (dispatcher, suffix) => { + ProcessOut.DynamicCheckoutEventsUtils[dispatcher](details) + + expect(lastEvent().type).toBe(`processout_dynamic_checkout_payment_${suffix}`) + }) + + it.each(objectDispatchers)("%s emits a card verification event when flagged", (d, suffix) => { + ProcessOut.DynamicCheckoutEventsUtils[d](details, true) + + expect(lastEvent().type).toBe(`processout_dynamic_checkout_card_verification_${suffix}`) + }) + + it.each(objectDispatchers)("%s sends the same detail in both modes", dispatcher => { + ProcessOut.DynamicCheckoutEventsUtils[dispatcher](details) + ProcessOut.DynamicCheckoutEventsUtils[dispatcher](details, true) + + expect(dispatchedEvents[1].detail).toEqual(dispatchedEvents[0].detail) + }) + + it("dispatchPaymentErrorEvent emits a payment error by default", () => { + ProcessOut.DynamicCheckoutEventsUtils.dispatchPaymentErrorEvent("iv_test", { + error_type: "card.declined", + }) + + expect(lastEvent().type).toBe("processout_dynamic_checkout_payment_error") + }) + + it("dispatchPaymentErrorEvent emits a card verification error when flagged", () => { + ProcessOut.DynamicCheckoutEventsUtils.dispatchPaymentErrorEvent( + "iv_test", + { error_type: "card.declined" }, + "card", + undefined, + null, + undefined, + "Card", + true, + ) + + expect(lastEvent().type).toBe("processout_dynamic_checkout_card_verification_error") + expect(lastEvent().detail.error_type).toBe("card.declined") + }) + + it("still swallows the field-unavailable error in verification mode", () => { + ProcessOut.DynamicCheckoutEventsUtils.dispatchPaymentErrorEvent( + "iv_test", + { error_type: "processout-js.field.unavailable" }, + "card", + undefined, + null, + undefined, + "Card", + true, + ) + + expect(dispatchedEvents).toHaveLength(0) + }) +}) diff --git a/test/support/loadNamespace.ts b/test/support/loadNamespace.ts index 546c1ce..e06ade4 100644 --- a/test/support/loadNamespace.ts +++ b/test/support/loadNamespace.ts @@ -46,13 +46,84 @@ export interface FakeNavigator { */ export function loadApmUtils(navigator: FakeNavigator = {}): Record { const js = compile("src/apm/utils.ts") + const moduleShim = { exports: {} as Record } + const run = new Function("module", "exports", "navigator", `${js}\nmodule.exports = ProcessOut;`) + run(moduleShim, moduleShim.exports, navigator) + return moduleShim.exports +} + +export const LOCALES = [ + "ar", + "de", + "en", + "es", + "fi", + "fr", + "it", + "ja", + "ko", + "nb", + "pl", + "pt", + "ta", + "vi", +] + +export interface CapturedEvent { + type: string + detail: any +} + +export interface DynamicCheckoutNamespace { + namespace: Record + dispatchedEvents: CapturedEvent[] +} + +/** + * Load the Dynamic Checkout locale/config/event helpers and return the resulting + * `ProcessOut` namespace, plus the list of events the code dispatched. + * + * Each file is transpiled in isolation, so cross-file references (a locale const + * used by `Translations`, `Translations` used by `getStatusMessage`) compile to + * bare identifiers rather than `ProcessOut.x`. The bridge lines below re-expose + * the namespace members under those names, in the same order the real + * `tsc --outFile` bundle concatenates them. + */ +export function loadDynamicCheckout(): DynamicCheckoutNamespace { + const chunks = [ + ...LOCALES.map(locale => compile(`src/dynamic-checkout/locales/${locale}.ts`)), + `const { ${LOCALES.join(", ")} } = ProcessOut;`, + compile("src/dynamic-checkout/utils/translations.ts"), + `const { Translations } = ProcessOut;`, + compile("src/dynamic-checkout/utils/status-messages.ts"), + compile("src/dynamic-checkout/config/payment-config.ts"), + compile("src/dynamic-checkout/utils/events.ts"), + ] + + const dispatchedEvents: CapturedEvent[] = [] + + function FakeCustomEvent(this: any, type: string, init: any) { + this.type = type + this.detail = init ? init.detail : undefined + } + + const windowShim = { + CustomEvent: FakeCustomEvent, + dispatchEvent: (event: CapturedEvent) => { + dispatchedEvents.push(event) + return true + }, + } + const moduleShim = { exports: {} as Record } const run = new Function( "module", "exports", - "navigator", - `${js}\nmodule.exports = ProcessOut;`, + "window", + "CustomEvent", + `${chunks.join("\n")}\nmodule.exports = ProcessOut;`, ) - run(moduleShim, moduleShim.exports, navigator) - return moduleShim.exports + run(moduleShim, moduleShim.exports, windowShim, FakeCustomEvent) + + return { namespace: moduleShim.exports, dispatchedEvents } }