web/satellite: use stripe payment element

This change uses the recommended stripe payment element to collect card
information instead of the legacy card element currently in use.

Issue: #6436

Change-Id: If931d47430940e0932c845b6eee3e0e23c294fbb
This commit is contained in:
Wilfred Asomani 2023-10-25 10:43:14 +00:00 committed by Storj Robot
parent 67f32bd519
commit 8f59535f95
11 changed files with 286 additions and 57 deletions

View File

@ -119,6 +119,27 @@ export class PaymentsHttpApi implements PaymentsApi {
return new ProjectUsagePriceModel();
}
/**
* Add payment method.
* @param pmID - stripe payment method id of the credit card
* @throws Error
*/
public async addCardByPaymentMethodID(pmID: string): Promise<void> {
const path = `${this.ROOT_PATH}/payment-methods`;
const response = await this.client.post(path, pmID);
if (response.ok) {
return;
}
const result = await response.json();
throw new APIError({
status: response.status,
message: result.error || 'Can not add payment method',
requestID: response.headers.get('x-request-id'),
});
}
/**
* Add credit card.
*
@ -474,12 +495,12 @@ export class PaymentsHttpApi implements PaymentsApi {
/**
* Purchases the pricing package associated with the user's partner.
*
* @param token - the Stripe token used to add a credit card as a payment method
* @param pmID - the Stripe payment method id of the credit card
* @throws Error
*/
public async purchasePricingPackage(token: string): Promise<void> {
const path = `${this.ROOT_PATH}/purchase-package`;
const response = await this.client.post(path, token);
public async purchasePricingPackage(pmID: string): Promise<void> {
const path = `${this.ROOT_PATH}/purchase-package?pmID=true`;
const response = await this.client.post(path, pmID);
if (response.ok) {
return;

View File

@ -27,7 +27,7 @@
@remove="removePaymentMethodHandler"
/>
</div>
<div class="payments-area__container__new-payments">
<div class="payments-area__container__new-payments" :class="{ 'white-background': isAddingPayment }">
<div v-if="!isAddingPayment" class="payments-area__container__new-payments__text-area">
<span>+&nbsp;</span>
<span
@ -41,10 +41,10 @@
</div>
<div class="payments-area__create-header">Credit Card</div>
<div class="payments-area__create-subheader">Add Card Info</div>
<StripeCardInput
<StripeCardElement
ref="stripeCardInput"
class="add-card-area__stripe stripe_input"
:on-stripe-response-callback="addCard"
@pm-created="addCard"
/>
<VButton
class="add-card-button"
@ -202,12 +202,12 @@ import { useLoading } from '@/composables/useLoading';
import VButton from '@/components/common/VButton.vue';
import VLoader from '@/components/common/VLoader.vue';
import CreditCardContainer from '@/components/account/billing/billingTabs/CreditCardContainer.vue';
import StripeCardInput from '@/components/account/billing/paymentMethods/StripeCardInput.vue';
import SortingHeader from '@/components/account/billing/billingTabs/SortingHeader.vue';
import AddTokenCard from '@/components/account/billing/paymentMethods/AddTokenCard.vue';
import AddTokenCardNative from '@/components/account/billing/paymentMethods/AddTokenCardNative.vue';
import TokenTransactionItem from '@/components/account/billing/paymentMethods/TokenTransactionItem.vue';
import VTable from '@/components/common/VTable.vue';
import StripeCardElement from '@/components/account/billing/paymentMethods/StripeCardElement.vue';
import CloseCrossIcon from '@/../static/images/common/closeCross.svg';
import AmericanExpressIcon from '@/../static/images/payments/cardIcons/smallamericanexpress.svg';
@ -381,12 +381,13 @@ function closeAddPayment(): void {
isAddingPayment.value = false;
}
async function addCard(token: string): Promise<void> {
async function addCard(pmID: string): Promise<void> {
try {
await billingStore.addCreditCard(token);
await billingStore.addCardByPaymentMethodID(pmID);
// We fetch User one more time to update their Paid Tier status.
await usersStore.getUser();
} catch (error) {
isLoading.value = false;
notify.notifyError(error, AnalyticsErrorEventSource.BILLING_PAYMENT_METHODS_TAB);
return;
}
@ -770,7 +771,7 @@ $align: center;
&__new-payments {
width: 348px;
height: 203px;
min-height: 203px;
padding: 24px;
box-sizing: border-box;
border: 2px dashed var(--c-grey-5);
@ -779,6 +780,10 @@ $align: center;
align-items: center;
justify-content: center;
&.white-background {
background: var(--c-white);
}
&__text-area {
display: flex;
align-items: center;

View File

@ -0,0 +1,185 @@
// Copyright (C) 2019 Storj Labs, Inc.
// See LICENSE for copying information.
<template>
<form id="payment-form">
<div class="form-row">
<div id="payment-element">
<!-- A Stripe Element will be inserted here. -->
</div>
<div id="card-errors" role="alert" />
</div>
</form>
</template>
<script setup lang="ts">
import { onMounted, ref, watch } from 'vue';
import { loadStripe } from '@stripe/stripe-js/pure';
import { Stripe, StripeElements, StripePaymentElement } from '@stripe/stripe-js';
import { StripeElementsOptionsMode } from '@stripe/stripe-js/types/stripe-js/elements-group';
import { AnalyticsErrorEventSource } from '@/utils/constants/analyticsEventNames';
import { useNotify } from '@/utils/hooks';
import { useConfigStore } from '@/store/modules/configStore';
import { useLoading } from '@/composables/useLoading';
import { useBillingStore } from '@/store/modules/billingStore';
const configStore = useConfigStore();
const billingStore = useBillingStore();
const notify = useNotify();
const { withLoading, isLoading } = useLoading();
const props = withDefaults(defineProps<{
isDarkTheme?: boolean
}>(), {
isDarkTheme: false,
});
const emit = defineEmits<{
(e: 'pmCreated', pmID: string): void
}>();
/**
* Stripe elements is used to create 'Add Card' form.
*/
const paymentElement = ref<StripePaymentElement>();
/**
* Stripe library.
*/
const stripe = ref<Stripe | null>(null);
const elements = ref<StripeElements | null>(null);
/**
* Stripe initialization.
*/
async function initStripe(): Promise<void> {
const stripePublicKey = configStore.state.config.stripePublicKey;
try {
stripe.value = await loadStripe(stripePublicKey);
} catch (error) {
notify.error(error.message, AnalyticsErrorEventSource.BILLING_STRIPE_CARD_INPUT);
return;
}
if (!stripe.value) {
notify.error('Unable to initialize stripe', AnalyticsErrorEventSource.BILLING_STRIPE_CARD_INPUT);
return;
}
const options: StripeElementsOptionsMode = {
mode: 'setup',
currency: 'usd',
paymentMethodCreation: 'manual',
paymentMethodTypes: ['card'],
appearance: {
theme: props.isDarkTheme ? 'night' : 'stripe',
labels: 'floating',
},
};
elements.value = stripe.value?.elements(options);
if (!elements.value) {
notify.error('Unable to instantiate elements', AnalyticsErrorEventSource.BILLING_STRIPE_CARD_INPUT);
return;
}
paymentElement.value = elements.value.create('payment');
if (!paymentElement.value) {
notify.error('Unable to create card element', AnalyticsErrorEventSource.BILLING_STRIPE_CARD_INPUT);
return;
}
paymentElement.value?.mount('#payment-element');
}
/**
* Fires stripe event after all inputs are filled.
*/
function onSubmit(): void {
const displayError: HTMLElement = document.getElementById('card-errors') as HTMLElement;
withLoading(async () => {
if (!(stripe.value && elements.value && paymentElement.value)) {
notify.error('Stripe is not initialized', AnalyticsErrorEventSource.BILLING_STRIPE_CARD_INPUT);
return;
}
// Trigger form validation
const res = await elements.value.submit();
if (res.error) {
displayError.textContent = res.error.message ?? '';
return;
}
// Create the PaymentMethod using the details collected by the Payment Element
const { error, paymentMethod } = await stripe.value.createPaymentMethod({
elements: elements.value,
});
if (error) {
displayError.textContent = error.message ?? '';
return;
}
if (paymentMethod.card?.funding === 'prepaid') {
displayError.textContent = 'Prepaid cards are not supported';
return;
}
emit('pmCreated', paymentMethod.id);
});
}
watch(() => props.isDarkTheme, isDarkTheme => {
elements.value?.update({
appearance: {
theme: isDarkTheme ? 'night' : 'stripe',
labels: 'floating',
},
});
});
/**
* Stripe library loading and initialization.
*/
onMounted(() => {
initStripe();
});
defineExpose({
onSubmit,
});
</script>
<style scoped lang="scss">
.StripeElement {
box-sizing: border-box;
width: 100%;
padding-bottom: 14px;
border-radius: 4px;
transition: box-shadow 150ms ease;
}
.StripeElement--focus {
box-shadow: 0 1px 3px 0 #cfd7df;
}
.StripeElement--invalid {
border-color: #fa755a;
}
.StripeElement--webkit-autofill {
background-color: #fefde5 !important;
}
.form-row {
width: 100%;
}
#card-errors {
text-align: left;
font-family: 'font-medium', sans-serif;
color: var(--c-red-2);
}
</style>

View File

@ -26,10 +26,10 @@
<div class="content__bottom">
<div v-if="!isFree" class="content__bottom__card-area">
<p class="content__bottom__card-area__label">Add Card Info</p>
<StripeCardInput
<StripeCardElement
ref="stripeCardInput"
class="content__bottom__card-area__input"
:on-stripe-response-callback="onCardAdded"
@pm-created="onCardAdded"
/>
</div>
<VButton
@ -89,9 +89,9 @@ import { useBillingStore } from '@/store/modules/billingStore';
import { useAppStore } from '@/store/modules/appStore';
import { useConfigStore } from '@/store/modules/configStore';
import StripeCardInput from '@/components/account/billing/paymentMethods/StripeCardInput.vue';
import VButton from '@/components/common/VButton.vue';
import VModal from '@/components/common/VModal.vue';
import StripeCardElement from '@/components/account/billing/paymentMethods/StripeCardElement.vue';
import CheckIcon from '@/../static/images/common/check.svg';
import CircleCheck from '@/../static/images/onboardingTour/circleCheck.svg';
@ -111,7 +111,7 @@ const notify = useNotify();
const isLoading = ref<boolean>(false);
const isSuccess = ref<boolean>(false);
const stripeCardInput = ref<(typeof StripeCardInput & StripeForm) | null>(null);
const stripeCardInput = ref<StripeForm | null>(null);
/**
* Returns the pricing plan selected from the onboarding tour.
@ -165,16 +165,16 @@ function onActivateClick(): void {
/**
* Adds card after Stripe confirmation.
*/
async function onCardAdded(token: string): Promise<void> {
async function onCardAdded(pmID: string): Promise<void> {
if (!plan.value) return;
let action = billingStore.addCreditCard;
let action = billingStore.addCardByPaymentMethodID;
if (plan.value.type === PricingPlanType.PARTNER) {
action = billingStore.purchasePricingPackage;
}
try {
await action(token);
await action(pmID);
isSuccess.value = true;
// Fetch user to update paid tier status

View File

@ -8,9 +8,9 @@
By saving your card information, you allow Storj to charge your card for future payments in accordance with
the terms.
</p>
<StripeCardInput
<StripeCardElement
ref="stripeCardInput"
:on-stripe-response-callback="addCardToDB"
@pm-created="addCardToDB"
/>
<VButton
class="button"
@ -42,8 +42,8 @@ import { useProjectsStore } from '@/store/modules/projectsStore';
import { useAnalyticsStore } from '@/store/modules/analyticsStore';
import UpgradeAccountWrapper from '@/components/modals/upgradeAccountFlow/UpgradeAccountWrapper.vue';
import StripeCardInput from '@/components/account/billing/paymentMethods/StripeCardInput.vue';
import VButton from '@/components/common/VButton.vue';
import StripeCardElement from '@/components/account/billing/paymentMethods/StripeCardElement.vue';
interface StripeForm {
onSubmit(): Promise<void>;
@ -62,7 +62,7 @@ const props = defineProps<{
}>();
const loading = ref<boolean>(false);
const stripeCardInput = ref<typeof StripeCardInput & StripeForm | null>(null);
const stripeCardInput = ref<StripeForm | null>(null);
/**
* Provides card information to Stripe.
@ -83,11 +83,11 @@ async function onSaveCardClick(): Promise<void> {
/**
* Adds card after Stripe confirmation.
*
* @param token from Stripe
* @param pmID from Stripe
*/
async function addCardToDB(token: string): Promise<void> {
async function addCardToDB(pmID: string): Promise<void> {
try {
await billingStore.addCreditCard(token);
await billingStore.addCardByPaymentMethodID(pmID);
notify.success('Card successfully added');
// We fetch User one more time to update their Paid Tier status.
await usersStore.getUser();

View File

@ -72,6 +72,10 @@ export const useBillingStore = defineStore('billing', () => {
await api.addCreditCard(token);
}
async function addCardByPaymentMethodID(pmID: string): Promise<void> {
await api.addCardByPaymentMethodID(pmID);
}
function toggleCardSelection(id: string): void {
state.creditCards = state.creditCards.map(card => {
if (card.id === id) {
@ -183,8 +187,8 @@ export const useBillingStore = defineStore('billing', () => {
state.coupon = await api.getCoupon();
}
async function purchasePricingPackage(token: string): Promise<void> {
await api.purchasePricingPackage(token);
async function purchasePricingPackage(pmID: string): Promise<void> {
await api.purchasePricingPackage(pmID);
}
function clear(): void {
@ -209,6 +213,7 @@ export const useBillingStore = defineStore('billing', () => {
setupAccount,
getCreditCards,
addCreditCard,
addCardByPaymentMethodID,
toggleCardSelection,
clearCardsSelection,
makeCardDefault,

View File

@ -49,6 +49,13 @@ export interface PaymentsApi {
*/
addCreditCard(token: string): Promise<void>;
/**
* Add payment method.
* @param pmID - stripe payment method id of the credit card
* @throws Error
*/
addCardByPaymentMethodID(pmID: string): Promise<void>;
/**
* Detach credit card from payment account.
* @param cardId
@ -128,10 +135,10 @@ export interface PaymentsApi {
/**
* Purchases the pricing package associated with the user's partner.
*
* @param token - the Stripe token used to add a credit card as a payment method
* @param pmID - the Stripe payment method id of the credit card
* @throws Error
*/
purchasePricingPackage(token: string): Promise<void>;
purchasePricingPackage(pmID: string): Promise<void>;
/**
* Returns whether there is a pricing package configured for the user's partner.

View File

@ -7,10 +7,10 @@
<v-btn v-if="!isCardInputShown" variant="outlined" color="default" size="small" class="mr-2" @click="isCardInputShown = true">+ Add New Card</v-btn>
<template v-else>
<StripeCardInput
<StripeCardElement
ref="stripeCardInput"
class="mb-4"
:on-stripe-response-callback="addCardToDB"
:is-dark-theme="theme.global.current.value.dark"
@pm-created="addCardToDB"
/>
</template>
@ -26,7 +26,6 @@
<v-btn
variant="outlined" color="default" size="small" class="mr-2"
:disabled="isLoading"
:loading="isLoading"
@click="isCardInputShown = false"
>
Cancel
@ -37,8 +36,9 @@
</template>
<script setup lang="ts">
import { VBtn, VCard, VCardText, VCardActions } from 'vuetify/components';
import { VBtn, VCard, VCardText } from 'vuetify/components';
import { ref } from 'vue';
import { useTheme } from 'vuetify';
import { useUsersStore } from '@/store/modules/usersStore';
import { useLoading } from '@/composables/useLoading';
@ -46,7 +46,7 @@ import { useNotify } from '@/utils/hooks';
import { useBillingStore } from '@/store/modules/billingStore';
import { AnalyticsErrorEventSource } from '@/utils/constants/analyticsEventNames';
import StripeCardInput from '@/components/account/billing/paymentMethods/StripeCardInput.vue';
import StripeCardElement from '@/components/account/billing/paymentMethods/StripeCardElement.vue';
interface StripeForm {
onSubmit(): Promise<void>;
@ -55,9 +55,10 @@ interface StripeForm {
const usersStore = useUsersStore();
const notify = useNotify();
const billingStore = useBillingStore();
const theme = useTheme();
const { isLoading } = useLoading();
const stripeCardInput = ref<typeof StripeCardInput & StripeForm | null>(null);
const stripeCardInput = ref<StripeForm | null>(null);
const isCardInputShown = ref(false);
@ -79,12 +80,12 @@ async function onSaveCardClick(): Promise<void> {
/**
* Adds card after Stripe confirmation.
*
* @param token from Stripe
* @param pmID - payment method ID from Stripe
*/
async function addCardToDB(token: string): Promise<void> {
async function addCardToDB(pmID: string): Promise<void> {
isLoading.value = true;
try {
await billingStore.addCreditCard(token);
await billingStore.addCardByPaymentMethodID(pmID);
notify.success('Card successfully added');
isCardInputShown.value = false;
isLoading.value = false;

View File

@ -10,9 +10,10 @@
<v-divider />
<div class="py-4">
<StripeCardInput
<StripeCardElement
ref="stripeCardInput"
:on-stripe-response-callback="addCardToDB"
:is-dark-theme="theme.global.current.value.dark"
@pm-created="addCardToDB"
/>
</div>
@ -51,6 +52,7 @@
import { ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { VDivider, VBtn, VIcon, VCol, VRow } from 'vuetify/components';
import { useTheme } from 'vuetify';
import { AnalyticsErrorEventSource, AnalyticsEvent } from '@/utils/constants/analyticsEventNames';
import { RouteConfig } from '@/types/router';
@ -60,7 +62,7 @@ import { useUsersStore } from '@/store/modules/usersStore';
import { useProjectsStore } from '@/store/modules/projectsStore';
import { useAnalyticsStore } from '@/store/modules/analyticsStore';
import StripeCardInput from '@/components/account/billing/paymentMethods/StripeCardInput.vue';
import StripeCardElement from '@/components/account/billing/paymentMethods/StripeCardElement.vue';
interface StripeForm {
onSubmit(): Promise<void>;
@ -71,6 +73,7 @@ const usersStore = useUsersStore();
const billingStore = useBillingStore();
const projectsStore = useProjectsStore();
const notify = useNotify();
const theme = useTheme();
const router = useRouter();
const route = useRoute();
@ -80,7 +83,7 @@ const emit = defineEmits<{
}>();
const loading = ref<boolean>(false);
const stripeCardInput = ref<typeof StripeCardInput & StripeForm | null>(null);
const stripeCardInput = ref<StripeForm | null>(null);
/**
* Provides card information to Stripe.
@ -101,12 +104,12 @@ async function onSaveCardClick(): Promise<void> {
/**
* Adds card after Stripe confirmation.
*
* @param token from Stripe
* @param pmID - payment method ID from Stripe
*/
async function addCardToDB(token: string): Promise<void> {
async function addCardToDB(pmID: string): Promise<void> {
loading.value = true;
try {
await billingStore.addCreditCard(token);
await billingStore.addCardByPaymentMethodID(pmID);
notify.success('Card successfully added');
// We fetch User one more time to update their Paid Tier status.
await usersStore.getUser();

View File

@ -33,10 +33,10 @@
<div v-if="!isFree" class="py-4">
<p class="text-caption">Add Card Info</p>
<StripeCardInput
<StripeCardElement
ref="stripeCardInput"
class="content__bottom__card-area__input"
:on-stripe-response-callback="onCardAdded"
:is-dark-theme="theme.global.current.value.dark"
@pm-created="onCardAdded"
/>
</div>
@ -110,6 +110,7 @@
import { computed, ref } from 'vue';
import { useRouter } from 'vue-router';
import { VBtn, VCol, VIcon, VRow } from 'vuetify/components';
import { useTheme } from 'vuetify';
import { PricingPlanInfo, PricingPlanType } from '@/types/common';
import { useNotify } from '@/utils/hooks';
@ -117,7 +118,7 @@ import { useUsersStore } from '@/store/modules/usersStore';
import { useBillingStore } from '@/store/modules/billingStore';
import { useConfigStore } from '@/store/modules/configStore';
import StripeCardInput from '@/components/account/billing/paymentMethods/StripeCardInput.vue';
import StripeCardElement from '@/components/account/billing/paymentMethods/StripeCardElement.vue';
interface StripeForm {
onSubmit(): Promise<void>;
@ -128,11 +129,12 @@ const billingStore = useBillingStore();
const usersStore = useUsersStore();
const router = useRouter();
const notify = useNotify();
const theme = useTheme();
const isLoading = ref<boolean>(false);
const isSuccess = ref<boolean>(false);
const stripeCardInput = ref<(typeof StripeCardInput & StripeForm) | null>(null);
const stripeCardInput = ref<StripeForm | null>(null);
const props = defineProps<{
plan: PricingPlanInfo;
@ -174,14 +176,14 @@ async function onActivateClick() {
/**
* Adds card after Stripe confirmation.
*/
async function onCardAdded(token: string): Promise<void> {
let action = billingStore.addCreditCard;
async function onCardAdded(pmID: string): Promise<void> {
let action = billingStore.addCardByPaymentMethodID;
if (props.plan.type === PricingPlanType.PARTNER) {
action = billingStore.purchasePricingPackage;
}
try {
await action(token);
await action(pmID);
isSuccess.value = true;
// Fetch user to update paid tier status

View File

@ -166,15 +166,15 @@
<v-window-item>
<v-row>
<v-col cols="12" sm="4">
<v-col cols="12" md="4" sm="6">
<StorjTokenCardComponent @historyClicked="goToTransactionsTab" />
</v-col>
<v-col v-for="(card, i) in creditCards" :key="i" cols="12" sm="4">
<v-col v-for="(card, i) in creditCards" :key="i" cols="12" md="4" sm="6">
<CreditCardComponent :card="card" />
</v-col>
<v-col cols="12" sm="4">
<v-col cols="12" md="4" sm="6">
<AddCreditCardComponent />
</v-col>
</v-row>