satellite/{web,console}: add config for payment element

This change adds a config flag for whether the stripe payment element
should be used to collect card info.

Change-Id: I301cf69e6f1b64350266e8f2286542b951e216c4
This commit is contained in:
Wilfred Asomani 2023-11-10 17:21:56 +00:00 committed by Storj Robot
parent 7c7123a156
commit dc28dadbc2
14 changed files with 158 additions and 38 deletions

View File

@ -27,6 +27,7 @@ type Config struct {
PlacementEdgeURLOverrides PlacementEdgeURLOverrides `help:"placement-specific edge service URL overrides in the format {\"placementID\": {\"authService\": \"...\", \"publicLinksharing\": \"...\", \"internalLinksharing\": \"...\"}, \"placementID2\": ...}"`
BlockExplorerURL string `help:"url of the transaction block explorer" default:"https://etherscan.io/"`
BillingFeaturesEnabled bool `help:"indicates if billing features should be enabled" default:"true"`
StripePaymentElementEnabled bool `help:"indicates whether the stripe payment element should be used to collect card info" default:"true"`
UsageLimits UsageLimitsConfig
Captcha CaptchaConfig
Session SessionConfig

View File

@ -51,6 +51,7 @@ type FrontendConfig struct {
ObjectBrowserPaginationEnabled bool `json:"objectBrowserPaginationEnabled"`
ObjectBrowserCardViewEnabled bool `json:"objectBrowserCardViewEnabled"`
BillingFeaturesEnabled bool `json:"billingFeaturesEnabled"`
StripePaymentElementEnabled bool `json:"stripePaymentElementEnabled"`
UnregisteredInviteEmailsEnabled bool `json:"unregisteredInviteEmailsEnabled"`
FreeTierInvitesEnabled bool `json:"freeTierInvitesEnabled"`
UserBalanceForUpgrade int64 `json:"userBalanceForUpgrade"`

View File

@ -753,6 +753,7 @@ func (server *Server) frontendConfigHandler(w http.ResponseWriter, r *http.Reque
ObjectBrowserPaginationEnabled: server.config.ObjectBrowserPaginationEnabled,
ObjectBrowserCardViewEnabled: server.config.ObjectBrowserCardViewEnabled,
BillingFeaturesEnabled: server.config.BillingFeaturesEnabled,
StripePaymentElementEnabled: server.config.StripePaymentElementEnabled,
UnregisteredInviteEmailsEnabled: server.config.UnregisteredInviteEmailsEnabled,
FreeTierInvitesEnabled: server.config.FreeTierInvitesEnabled,
UserBalanceForUpgrade: server.config.UserBalanceForUpgrade,

View File

@ -403,6 +403,9 @@ compensation.withheld-percents: 75,75,75,50,50,50,25,25,25,0,0,0,0,0,0
# path to static resources
# console.static-dir: ""
# indicates whether the stripe payment element should be used to collect card info
# console.stripe-payment-element-enabled: true
# url link to terms and conditions page
# console.terms-and-conditions-url: https://www.storj.io/terms-of-service/

View File

@ -495,12 +495,13 @@ export class PaymentsHttpApi implements PaymentsApi {
/**
* Purchases the pricing package associated with the user's partner.
*
* @param pmID - the Stripe payment method id of the credit card
* @param dataStr - the Stripe payment method id or token of the credit card
* @param isPMID - whether the dataStr is a payment method id or token
* @throws Error
*/
public async purchasePricingPackage(pmID: string): Promise<void> {
const path = `${this.ROOT_PATH}/purchase-package?pmID=true`;
const response = await this.client.post(path, pmID);
public async purchasePricingPackage(dataStr: string, isPMID: boolean): Promise<void> {
const path = `${this.ROOT_PATH}/purchase-package?pmID=${isPMID}`;
const response = await this.client.post(path, dataStr);
if (response.ok) {
return;

View File

@ -42,10 +42,16 @@
<div class="payments-area__create-header">Credit Card</div>
<div class="payments-area__create-subheader">Add Card Info</div>
<StripeCardElement
v-if="paymentElementEnabled"
ref="stripeCardInput"
class="add-card-area__stripe stripe_input"
@pm-created="addCard"
/>
<StripeCardInput
v-else
ref="stripeCardInput"
:on-stripe-response-callback="addCard"
/>
<VButton
class="add-card-button"
label="Add Credit Card"
@ -208,6 +214,7 @@ import AddTokenCardNative from '@/components/account/billing/paymentMethods/AddT
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 StripeCardInput from '@/components/account/billing/paymentMethods/StripeCardInput.vue';
import CloseCrossIcon from '@/../static/images/common/closeCross.svg';
import AmericanExpressIcon from '@/../static/images/payments/cardIcons/smallamericanexpress.svg';
@ -253,13 +260,20 @@ const displayedHistory = ref<NativePaymentHistoryItem[]>([]);
const transactionCount = ref<number>(0);
const defaultCreditCardSelection = ref<string>('');
const cardBeingEdited = ref<CardEdited>({});
const stripeCardInput = ref<typeof StripeCardInput & StripeForm>();
const stripeCardInput = ref<StripeForm>();
const canvas = ref<HTMLCanvasElement>();
const pageCount = computed((): number => {
return Math.ceil(transactionCount.value / DEFAULT_PAGE_LIMIT);
});
/**
* Indicates whether stripe payment element is enabled.
*/
const paymentElementEnabled = computed(() => {
return configStore.state.config.stripePaymentElementEnabled;
});
/**
* Indicates whether native token payments are enabled.
*/
@ -381,15 +395,25 @@ function closeAddPayment(): void {
isAddingPayment.value = false;
}
async function addCard(pmID: string): Promise<void> {
/**
* Adds card after Stripe confirmation.
*
* @param res - the response from stripe. Could be a token or a payment method id.
* depending on the paymentElementEnabled flag.
*/
async function addCard(res: string): Promise<void> {
isLoading.value = true;
try {
await billingStore.addCardByPaymentMethodID(pmID);
const action = paymentElementEnabled.value ? billingStore.addCardByPaymentMethodID : billingStore.addCreditCard;
await action(res);
closeAddPayment();
// 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;
} finally {
isLoading.value = false;
}
notify.success('Card successfully added');
@ -408,7 +432,8 @@ async function onConfirmAddStripe(): Promise<void> {
if (isLoading.value || !stripeCardInput.value) return;
isLoading.value = true;
await stripeCardInput.value.onSubmit().then(() => {isLoading.value = false;});
await stripeCardInput.value.onSubmit();
isLoading.value = false;
analyticsStore.eventTriggered(AnalyticsEvent.CREDIT_CARD_ADDED_FROM_BILLING);
}

View File

@ -27,10 +27,16 @@
<div v-if="!isFree" class="content__bottom__card-area">
<p class="content__bottom__card-area__label">Add Card Info</p>
<StripeCardElement
v-if="paymentElementEnabled"
ref="stripeCardInput"
class="content__bottom__card-area__input"
@pm-created="onCardAdded"
/>
<StripeCardInput
v-else
ref="stripeCardInput"
:on-stripe-response-callback="onCardAdded"
/>
</div>
<VButton
class="content__bottom__button"
@ -92,6 +98,7 @@ import { useConfigStore } from '@/store/modules/configStore';
import VButton from '@/components/common/VButton.vue';
import VModal from '@/components/common/VModal.vue';
import StripeCardElement from '@/components/account/billing/paymentMethods/StripeCardElement.vue';
import StripeCardInput from '@/components/account/billing/paymentMethods/StripeCardInput.vue';
import CheckIcon from '@/../static/images/common/check.svg';
import CircleCheck from '@/../static/images/onboardingTour/circleCheck.svg';
@ -120,6 +127,13 @@ const plan = computed((): PricingPlanInfo | null => {
return appStore.state.selectedPricingPlan;
});
/**
* Indicates whether stripe payment element is enabled.
*/
const paymentElementEnabled = computed(() => {
return configStore.state.config.stripePaymentElementEnabled;
});
watch(plan, () => {
if (!plan.value) {
appStore.removeActiveModal();
@ -164,17 +178,18 @@ function onActivateClick(): void {
/**
* Adds card after Stripe confirmation.
* @param res - the response from stripe. Could be a token or a payment method id.
* depending on the paymentElementEnabled flag.
*/
async function onCardAdded(pmID: string): Promise<void> {
async function onCardAdded(res: string): Promise<void> {
if (!plan.value) return;
let action = billingStore.addCardByPaymentMethodID;
if (plan.value.type === PricingPlanType.PARTNER) {
action = billingStore.purchasePricingPackage;
}
try {
await action(pmID);
if (plan.value.type === PricingPlanType.PARTNER) {
await billingStore.purchasePricingPackage(res, paymentElementEnabled.value);
} else {
paymentElementEnabled.value ? await billingStore.addCardByPaymentMethodID(res) : await billingStore.addCreditCard(res);
}
isSuccess.value = true;
// Fetch user to update paid tier status

View File

@ -9,9 +9,15 @@
the terms.
</p>
<StripeCardElement
v-if="paymentElementEnabled"
ref="stripeCardInput"
@pm-created="addCardToDB"
/>
<StripeCardInput
v-else
ref="stripeCardInput"
:on-stripe-response-callback="addCardToDB"
/>
<VButton
class="button"
label="Save card"
@ -30,7 +36,7 @@
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { computed, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { AnalyticsErrorEventSource, AnalyticsEvent } from '@/utils/constants/analyticsEventNames';
@ -40,10 +46,12 @@ import { useBillingStore } from '@/store/modules/billingStore';
import { useUsersStore } from '@/store/modules/usersStore';
import { useProjectsStore } from '@/store/modules/projectsStore';
import { useAnalyticsStore } from '@/store/modules/analyticsStore';
import { useConfigStore } from '@/store/modules/configStore';
import UpgradeAccountWrapper from '@/components/modals/upgradeAccountFlow/UpgradeAccountWrapper.vue';
import VButton from '@/components/common/VButton.vue';
import StripeCardElement from '@/components/account/billing/paymentMethods/StripeCardElement.vue';
import StripeCardInput from '@/components/account/billing/paymentMethods/StripeCardInput.vue';
interface StripeForm {
onSubmit(): Promise<void>;
@ -52,6 +60,7 @@ interface StripeForm {
const analyticsStore = useAnalyticsStore();
const usersStore = useUsersStore();
const billingStore = useBillingStore();
const configStore = useConfigStore();
const projectsStore = useProjectsStore();
const notify = useNotify();
const router = useRouter();
@ -64,6 +73,13 @@ const props = defineProps<{
const loading = ref<boolean>(false);
const stripeCardInput = ref<StripeForm | null>(null);
/**
* Indicates whether stripe payment element is enabled.
*/
const paymentElementEnabled = computed(() => {
return configStore.state.config.stripePaymentElementEnabled;
});
/**
* Provides card information to Stripe.
*/
@ -83,11 +99,13 @@ async function onSaveCardClick(): Promise<void> {
/**
* Adds card after Stripe confirmation.
*
* @param pmID from Stripe
* @param res - the response from stripe. Could be a token or a payment method id.
* depending on the paymentElementEnabled flag.
*/
async function addCardToDB(pmID: string): Promise<void> {
async function addCardToDB(res: string): Promise<void> {
try {
await billingStore.addCardByPaymentMethodID(pmID);
const action = paymentElementEnabled.value ? billingStore.addCardByPaymentMethodID : billingStore.addCreditCard;
await action(res);
notify.success('Card successfully added');
// We fetch User one more time to update their Paid Tier status.
await usersStore.getUser();

View File

@ -187,8 +187,8 @@ export const useBillingStore = defineStore('billing', () => {
state.coupon = await api.getCoupon();
}
async function purchasePricingPackage(pmID: string): Promise<void> {
await api.purchasePricingPackage(pmID);
async function purchasePricingPackage(dataStr: string, isPMID: boolean): Promise<void> {
await api.purchasePricingPackage(dataStr, isPMID);
}
function clear(): void {

View File

@ -48,6 +48,7 @@ export class FrontendConfig {
objectBrowserPaginationEnabled: boolean;
objectBrowserCardViewEnabled: boolean;
billingFeaturesEnabled: boolean;
stripePaymentElementEnabled: boolean;
unregisteredInviteEmailsEnabled: boolean;
freeTierInvitesEnabled: boolean;
userBalanceForUpgrade: number;

View File

@ -135,10 +135,11 @@ export interface PaymentsApi {
/**
* Purchases the pricing package associated with the user's partner.
*
* @param pmID - the Stripe payment method id of the credit card
* @param dataStr - the Stripe payment method id or token of the credit card
* @param isPMID - whether the dataStr is a payment method id or token
* @throws Error
*/
purchasePricingPackage(pmID: string): Promise<void>;
purchasePricingPackage(dataStr: string, isPMID: boolean): Promise<void>;
/**
* Returns whether there is a pricing package configured for the user's partner.

View File

@ -8,10 +8,17 @@
<template v-else>
<StripeCardElement
v-if="paymentElementEnabled"
ref="stripeCardInput"
:is-dark-theme="theme.global.current.value.dark"
@pm-created="addCardToDB"
/>
<StripeCardInput
v-else
ref="stripeCardInput"
class="mb-4"
:on-stripe-response-callback="addCardToDB"
/>
</template>
<template v-if="isCardInputShown">
@ -37,7 +44,7 @@
<script setup lang="ts">
import { VBtn, VCard, VCardText } from 'vuetify/components';
import { ref } from 'vue';
import { computed, ref } from 'vue';
import { useTheme } from 'vuetify';
import { useUsersStore } from '@/store/modules/usersStore';
@ -45,13 +52,16 @@ import { useLoading } from '@/composables/useLoading';
import { useNotify } from '@/utils/hooks';
import { useBillingStore } from '@/store/modules/billingStore';
import { AnalyticsErrorEventSource } from '@/utils/constants/analyticsEventNames';
import { useConfigStore } from '@/store/modules/configStore';
import StripeCardElement from '@/components/account/billing/paymentMethods/StripeCardElement.vue';
import StripeCardInput from '@/components/account/billing/paymentMethods/StripeCardInput.vue';
interface StripeForm {
onSubmit(): Promise<void>;
}
const configStore = useConfigStore();
const usersStore = useUsersStore();
const notify = useNotify();
const billingStore = useBillingStore();
@ -62,6 +72,12 @@ const stripeCardInput = ref<StripeForm | null>(null);
const isCardInputShown = ref(false);
/**
* Indicates whether stripe payment element is enabled.
*/
const paymentElementEnabled = computed(() => {
return configStore.state.config.stripePaymentElementEnabled;
});
/**
* Provides card information to Stripe.
*/
@ -80,12 +96,14 @@ async function onSaveCardClick(): Promise<void> {
/**
* Adds card after Stripe confirmation.
*
* @param pmID - payment method ID from Stripe
* @param res - the response from stripe. Could be a token or a payment method id.
* depending on the paymentElementEnabled flag.
*/
async function addCardToDB(pmID: string): Promise<void> {
async function addCardToDB(res: string): Promise<void> {
isLoading.value = true;
try {
await billingStore.addCardByPaymentMethodID(pmID);
const action = paymentElementEnabled.value ? billingStore.addCardByPaymentMethodID : billingStore.addCreditCard;
await action(res);
notify.success('Card successfully added');
isCardInputShown.value = false;
isLoading.value = false;

View File

@ -9,10 +9,16 @@
<div class="py-4">
<StripeCardElement
v-if="paymentElementEnabled"
ref="stripeCardInput"
:is-dark-theme="theme.global.current.value.dark"
@pm-created="addCardToDB"
/>
<StripeCardInput
v-else
ref="stripeCardInput"
:on-stripe-response-callback="addCardToDB"
/>
</div>
<div class="pt-4">
@ -47,7 +53,7 @@
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { computed, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { VDivider, VBtn, VIcon, VCol, VRow } from 'vuetify/components';
import { useTheme } from 'vuetify';
@ -59,14 +65,17 @@ import { useBillingStore } from '@/store/modules/billingStore';
import { useUsersStore } from '@/store/modules/usersStore';
import { useProjectsStore } from '@/store/modules/projectsStore';
import { useAnalyticsStore } from '@/store/modules/analyticsStore';
import { useConfigStore } from '@/store/modules/configStore';
import StripeCardElement from '@/components/account/billing/paymentMethods/StripeCardElement.vue';
import StripeCardInput from '@/components/account/billing/paymentMethods/StripeCardInput.vue';
interface StripeForm {
onSubmit(): Promise<void>;
}
const analyticsStore = useAnalyticsStore();
const configStore = useConfigStore();
const usersStore = useUsersStore();
const billingStore = useBillingStore();
const projectsStore = useProjectsStore();
@ -83,6 +92,13 @@ const emit = defineEmits<{
const loading = ref<boolean>(false);
const stripeCardInput = ref<StripeForm | null>(null);
/**
* Indicates whether stripe payment element is enabled.
*/
const paymentElementEnabled = computed(() => {
return configStore.state.config.stripePaymentElementEnabled;
});
/**
* Provides card information to Stripe.
*/
@ -102,12 +118,14 @@ async function onSaveCardClick(): Promise<void> {
/**
* Adds card after Stripe confirmation.
*
* @param pmID - payment method ID from Stripe
* @param res - the response from stripe. Could be a token or a payment method id.
* depending on the paymentElementEnabled flag.
*/
async function addCardToDB(pmID: string): Promise<void> {
async function addCardToDB(res: string): Promise<void> {
loading.value = true;
try {
await billingStore.addCardByPaymentMethodID(pmID);
const action = paymentElementEnabled.value ? billingStore.addCardByPaymentMethodID : billingStore.addCreditCard;
await action(res);
notify.success('Card successfully added');
// We fetch User one more time to update their Paid Tier status.
await usersStore.getUser();

View File

@ -34,10 +34,16 @@
<div v-if="!isFree" class="py-4">
<p class="text-caption">Add Card Info</p>
<StripeCardElement
v-if="paymentElementEnabled"
ref="stripeCardInput"
:is-dark-theme="theme.global.current.value.dark"
@pm-created="onCardAdded"
/>
<StripeCardInput
v-else
ref="stripeCardInput"
:on-stripe-response-callback="onCardAdded"
/>
</div>
<div class="py-4">
@ -119,6 +125,7 @@ import { useBillingStore } from '@/store/modules/billingStore';
import { useConfigStore } from '@/store/modules/configStore';
import StripeCardElement from '@/components/account/billing/paymentMethods/StripeCardElement.vue';
import StripeCardInput from '@/components/account/billing/paymentMethods/StripeCardInput.vue';
interface StripeForm {
onSubmit(): Promise<void>;
@ -145,6 +152,13 @@ const emit = defineEmits<{
close: [];
}>();
/**
* Indicates whether stripe payment element is enabled.
*/
const paymentElementEnabled = computed(() => {
return configStore.state.config.stripePaymentElementEnabled;
});
/**
* Returns whether current plan is a free pricing plan.
*/
@ -175,15 +189,18 @@ async function onActivateClick() {
/**
* Adds card after Stripe confirmation.
* @param res - the response from stripe. Could be a token or a payment method id.
* depending on the paymentElementEnabled flag.
*/
async function onCardAdded(pmID: string): Promise<void> {
let action = billingStore.addCardByPaymentMethodID;
if (props.plan.type === PricingPlanType.PARTNER) {
action = billingStore.purchasePricingPackage;
}
async function onCardAdded(res: string): Promise<void> {
if (!props.plan) return;
try {
await action(pmID);
if (props.plan.type === PricingPlanType.PARTNER) {
await billingStore.purchasePricingPackage(res, paymentElementEnabled.value);
} else {
paymentElementEnabled.value ? await billingStore.addCardByPaymentMethodID(res) : await billingStore.addCreditCard(res);
}
isSuccess.value = true;
// Fetch user to update paid tier status