Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: AddPaymentMethodForm shows a more end-user-friendly error message when it encounters an error #2058

Merged
merged 1 commit into from Oct 20, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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)) {
drewdelano marked this conversation as resolved.
Show resolved Hide resolved
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;
}