Skip to content

Commit

Permalink
feat: AddPaymentMethodForm shows a more end-user-friendly error messa…
Browse files Browse the repository at this point in the history
…ge when it encounters an error (#2058)

Motivation:
* provide a more helpful error message in situations like this
#1987

How:
* If the AddPaymentForm encounters an error, it doesn't render the error
message (that is probably more intended for a developer to debug than
the end-user), it renders in the html a message that gives the user an
affordance to get in touch with us if the error keeps happening, and
doesn't show any dev details. Dev details still appear in the dev
console to aid debugging.
  • Loading branch information
gobengo committed Oct 20, 2022
1 parent 3464807 commit 376e05b
Show file tree
Hide file tree
Showing 2 changed files with 66 additions and 19 deletions.
Expand Up @@ -2,7 +2,7 @@ import { useState } from 'react';
import { CardElement, useElements, useStripe } from '@stripe/react-stripe-js';

import Loading from '../../../components/loading/loading';
import { userBillingSettings } from '../../../lib/api';
import { APIError, defaultErrorMessageForEndUser, userBillingSettings } from '../../../lib/api';
import Button from '../../../components/button/button';
import { planIdToStorageSubscription } from '../../contexts/plansContext';

Expand All @@ -22,7 +22,7 @@ const AddPaymentMethodForm = ({ setHasPaymentMethods, setEditingPaymentMethod, c
const stripe = useStripe();
const [isLoading, setIsLoading] = useState(false);

const [paymentMethodError, setPaymentMethodError] = useState('');
const [paymentMethodError, setPaymentMethodError] = useState(/** @type {Error|null} */ (null));
const handlePaymentMethodAdd = async event => {
event.preventDefault();

Expand Down Expand Up @@ -52,12 +52,12 @@ const AddPaymentMethodForm = ({ setHasPaymentMethods, setEditingPaymentMethod, c
await userBillingSettings(paymentMethod.id, currStorageSubscription);
setHasPaymentMethods?.(true);
setEditingPaymentMethod?.(false);
setPaymentMethodError('');
setPaymentMethodError(null);
} catch (error) {
let message;
if (error instanceof Error) message = error.message;
else message = String(error);
setPaymentMethodError(message);
if (!(error instanceof APIError)) {
console.warn('unexpected error adding payment method', error);
}
setPaymentMethodError(new Error(defaultErrorMessageForEndUser));
} finally {
setIsLoading(false);
}
Expand All @@ -81,7 +81,7 @@ const AddPaymentMethodForm = ({ setHasPaymentMethods, setEditingPaymentMethod, c
}}
/>
</div>
<div className="billing-validation">{paymentMethodError}</div>
<div className="billing-validation">{paymentMethodError ? paymentMethodError.message : <></>}</div>
<div className="billing-card-add-card-wrapper">
<Button onClick={handlePaymentMethodAdd} variant="outline-light" disabled={!stripe}>
Add Card
Expand Down
69 changes: 58 additions & 11 deletions packages/website/lib/api.js
Expand Up @@ -305,6 +305,42 @@ export async function deletePinRequest(requestid) {
* } EarlyAdopterStorageSubscription
*/

/**
* Try return the input parsed as JSON. If it's not possible, return the input.
* (This is useful for logging responses that may or may not be JSON.)
* @param {string} text
* @returns {unknown}
*/
function parseJsonOrReturn(text) {
try {
return JSON.parse(text);
} catch (e) {
return text;
}
}

export const apiIssueSupportEmail = 'support@web3.storage';
export const defaultErrorMessageForEndUser = `An unexpected error has occurred. Please try again. If this error continues to happen, please contact us at ${apiIssueSupportEmail}.`;

/**
* Error indicating that an unexpected error happened when invoking the API, but nothing more specific than that.
*/
export class APIError extends Error {}

/**
* Error indicating that an unexpected API Response was encountered
*/
export class UnexpectedAPIResponseError extends APIError {
/**
* @param {Response} response
* @param {string} message
*/
constructor(response, message) {
super(message);
this.response = response;
}
}

/**
* Gets/Puts saved user plan and billing settings.
* @param {string} [pmId] - payment method id
Expand All @@ -320,17 +356,28 @@ export async function userBillingSettings(pmId, storageSubscription) {
: null;
const method = !!putBody ? 'PUT' : 'GET';
const token = await getToken();
const res = await fetch(API + '/user/payment', {
method: method,
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer ' + token,
},
body: putBody,
});
if (!res.ok) {
throw new Error(`failed to get/put user billing info: ${await res.text()}`);
const path = '/user/payment';
/** @type {Response} */
let response;
try {
response = await fetch(new URL(path, API).toString(), {
method: method,
headers: {
'Content-Type': 'application/json',
Authorization: 'Bearer ' + token,
},
body: putBody,
});
} catch (error) {
const message = 'unexpected error fetching user payment settings';
console.warn(message, error);
throw new APIError(message);
}
if (!response.ok) {
const message = `Unexpected ${response.status} response requesting ${method} ${path}`;
console.warn(`${message}. Response:`, await response.text().then(parseJsonOrReturn));
throw new UnexpectedAPIResponseError(response, message);
}
const savedPayment = await res.json();
const savedPayment = await response.json();
return savedPayment;
}

0 comments on commit 376e05b

Please sign in to comment.