Skip to content

Commit

Permalink
Add payment error messages
Browse files Browse the repository at this point in the history
- fixes #1589
  • Loading branch information
dave justice committed Jul 10, 2019
1 parent 56ab3fd commit abba789
Show file tree
Hide file tree
Showing 5 changed files with 129 additions and 14 deletions.
@@ -1,6 +1,6 @@
#dialogs {
.modal {
padding: 64px 27px;
padding: 25px 27px;
text-align: center;
}

Expand Down
11 changes: 11 additions & 0 deletions packages/fxa-payments-server/src/lib/errors.test.tsx
@@ -0,0 +1,11 @@
import 'jest-dom/extend-expect';
import { getErrorMessage, BASIC_ERROR, PAYMENT_ERROR_1 } from './errors';

it('returns the basic error text if not predefined error type', () => {
expect(getErrorMessage("NON_PREDEFINED_ERROR")).toEqual(BASIC_ERROR);
});

it('returns the payment error text for the correct error type', () => {
expect(getErrorMessage("approve_with_id")).toEqual(PAYMENT_ERROR_1);
});

105 changes: 103 additions & 2 deletions packages/fxa-payments-server/src/lib/errors.ts
@@ -1,9 +1,110 @@
// ref: fxa-auth-server/lib/error.js
export const AuthServerErrno = {
const AuthServerErrno = {
UNKNOWN_SUBSCRIPTION_CUSTOMER: 176,
UNKNOWN_SUBSCRIPTION: 177,
UNKNOWN_SUBSCRIPTION_PLAN: 178,
REJECTED_SUBSCRIPTION_PAYMENT_TOKEN: 179,
SUBSCRIPTION_ALREADY_CANCELLED: 180,
REJECTED_CUSTOMER_UPDATE: 181,
};
};

/*
* Todos:
* - L10N
* - handle General SubHub subscription creation failure on submit
* (network issue, server error)
* - handle Payment token not valid
*/

const BASIC_ERROR = "Hmm, we're having trouble with our system. We're working on fixing it for you and apologize for the inconvenience. Please try again later.";
const PAYMENT_ERROR_1 = "Hmm. There was a problem authorizing your payment. Try again or get in touch with your card issuer.";
const PAYMENT_ERROR_2 = "Hmm. There was a problem authorizing your payment. Get in touch with your card issuer.";

let errorMessageIndex: { [key: string]: string } = {
"expired_card": "It looks like your credit card has expired. Try another card.",
"insufficient_funds": "It looks like your card has insufficient funds. Try another card.",
"withdrawal_count_limit_exceeded": "It looks like this transaction will put you over your credit limit. Try another card.",
"charge_exceeds_source_limit": "It looks like this transaction will put you over your daily credit limit. Try another card or in 24 hours.",
"instant_payouts_unsupported": "It looks like your debit card isn't setup for instant payments. Try another debit or credit card.",
"duplicate_transaction": "Hmm. Looks like an identical transaction was just sent. Check your payment history.",
"coupon_expired": "It looks like that promo code has expired.",
// todo: handle "parameters_exclusive": "Your already subscribed to _product_"
};

const basicErrors = ["api_key_expired",
"platform_api_key_expired",
"rate_limit",
"UNKNOWN", // TODO: General SubHub subscription creation failure on submit (network issue, server error)
"api_connection_error",
"api_error",
"invalid_request_error",
"UNKNOWN", // TODO: Payment token not valid
"state_unsupported",
"invalid_source_usage",
"invoice_no_customer_line_items",
"invoice_no_subscription_line_items",
"invoice_not_editable",
"invoice_upcoming_none",
"missing",
"order_creation_failed",
"order_required_settings",
"order_status_invalid",
"order_upstream_timeout",
"payment_intent_incompatible_payment_method",
"payment_intent_unexpected_state",
"payment_method_unactivated",
"payment_method_unexpected_state",
"payouts_not_allowed",
"resource_already_exists",
"resource_missing",
"secret_key_required",
"sepa_unsupported_account",
"shipping_calculation_failed",
"tax_id_invalid",
"taxes_calculation_failed",
"tls_version_unsupported",
"token_already_used",
"token_in_use",
"transfers_not_allowed"];

const paymentErrors1 = ["approve_with_id",
"issuer_not_available",
"processing_error",
"reenter_transaction",
"try_again_later",
"payment_intent_authentication_failure",
"processing_error",];

const paymentErrors2 = ["call_issuer",
"card_not_supported",
"card_velocity_exceeded",
"do_not_honor",
"do_not_try_again",
"fraudulent",
"generic_decline",
"invalid_account",
"lost_card",
"merchant_blacklist",
"new_account_information_available",
"no_action_taken",
"not_permitted",
"pickup_card",
"restricted_card",
"revocation_of_all_authorizations",
"revocation_of_authorization",
"security_violation",
"service_not_allowed",
"stolen_card",
"stop_payment_order",
"transaction_not_allowed",];

basicErrors.forEach(k => errorMessageIndex[k] = BASIC_ERROR);
paymentErrors1.forEach(k => errorMessageIndex[k] = PAYMENT_ERROR_1);
paymentErrors2.forEach(k => errorMessageIndex[k] = PAYMENT_ERROR_2);

function getErrorMessage(type: string) {
return type ? errorMessageIndex[type] : BASIC_ERROR;
}

// BASIC_ERROR and PAYMENT_ERROR_1 are exported for errors.test.tsx
export { AuthServerErrno, getErrorMessage, BASIC_ERROR, PAYMENT_ERROR_1 };
13 changes: 7 additions & 6 deletions packages/fxa-payments-server/src/routes/Product/index.tsx
@@ -1,6 +1,6 @@
import React, { useEffect, useState, useCallback, useContext } from 'react';
import { connect } from 'react-redux';
import { AuthServerErrno } from '../../lib/errors';
import { AuthServerErrno, getErrorMessage } from '../../lib/errors';
import { actions, selectors } from '../../store';
import { AppContext } from '../../lib/AppContext';
import { LoadingOverlay } from '../../components/LoadingOverlay';
Expand Down Expand Up @@ -76,7 +76,7 @@ export const Product = ({
activated: accountActivated = false
} = queryParams;

const [ createTokenError, setCreateTokenError ] = useState({ message: null });
const [ createTokenError, setCreateTokenError ] = useState({ type: "", error: false });

// Fetch plans on initial render, change in product ID, or auth change.
useEffect(() => {
Expand Down Expand Up @@ -106,12 +106,13 @@ export const Product = ({
} else {
// This shouldn't happen with a successful createToken() call, but let's
// display an error in case it does.
const error: any = { message: 'No token response received from Stripe' };
const error: any = { type: 'api_error', error: true };
setCreateTokenError(error);
}
}, [ accessToken, selectedPlan, createSubscription, setCreateTokenError ]);

const onPaymentError = useCallback((error: any) => {
error.error = true;
setCreateTokenError(error);
}, [ setCreateTokenError ]);

Expand Down Expand Up @@ -187,16 +188,16 @@ export const Product = ({
error={createSubscriptionStatus.error} />
)}

{createTokenError.message && (
{createTokenError.error && (
<DialogMessage
className="dialog-error"
onDismiss={() => {
resetCreateSubscriptionError();
setCreateTokenError({ message: null });
setCreateTokenError({ type: "", error: false });
}}
>
<h4>Payment submission failed</h4>
<p>{createTokenError.message}</p>
<p>{getErrorMessage(createTokenError.type)}</p>
</DialogMessage>
)}

Expand Down
@@ -1,6 +1,7 @@
import React, { useCallback, useState } from 'react';
import dayjs from 'dayjs';
import { useBooleanState } from '../../lib/hooks';
import { getErrorMessage } from '../../lib/errors';
import {
Customer,
UpdatePaymentFetchState,
Expand Down Expand Up @@ -31,7 +32,7 @@ export const PaymentUpdateForm = ({
plan,
}: PaymentUpdateFormProps) => {
const [ updateRevealed, revealUpdate, hideUpdate ] = useBooleanState();
const [ createTokenError, setCreateTokenError ] = useState({ message: null });
const [ createTokenError, setCreateTokenError ] = useState({ type: "", error: false });
const onRevealUpdateClick = useCallback(() => {
resetUpdatePayment();
revealUpdate();
Expand All @@ -45,17 +46,18 @@ export const PaymentUpdateForm = ({
} else {
// This shouldn't happen with a successful createToken() call, but let's
// display an error in case it does.
const error: any = { message: 'No token response received from Stripe' };
const error: any = { type: 'api_error', error: true };
setCreateTokenError(error);
}
}, [ accessToken, updatePayment, setCreateTokenError ]);

const onPaymentError = useCallback((error: any) => {
error.error = true;
setCreateTokenError(error);
}, [ setCreateTokenError ]);

const onTokenErrorDismiss = useCallback(() => {
setCreateTokenError({ message: null });
setCreateTokenError({ type: "", error: false });
}, [ setCreateTokenError ]);

const inProgress =
Expand All @@ -76,10 +78,10 @@ export const PaymentUpdateForm = ({
return (
<div className="payment-update">

{createTokenError.message && (
{createTokenError.error && (
<DialogMessage className="dialog-error" onDismiss={onTokenErrorDismiss}>
<h4>Payment submission failed</h4>
<p>{createTokenError.message}</p>
<p>{getErrorMessage(createTokenError.type)}</p>
</DialogMessage>
)}

Expand Down

0 comments on commit abba789

Please sign in to comment.