web/satellite: use stripe as ES module

Start using @stripe/stripe-js lib (ES module) instead of regular stripe dependency.
Use strict typing for stripe commands/events.
This lib makes us able to modify stripe input styling in the future.

Change-Id: Iaba4f32a42e87edc85a4fbad82e5107c21bf19b6
This commit is contained in:
Vitalii 2023-08-28 16:51:52 +03:00 committed by Storj Robot
parent f1e8cdfe3e
commit f0829d5961
4 changed files with 445 additions and 427 deletions

File diff suppressed because it is too large Load Diff

View File

@ -23,13 +23,13 @@
"@hcaptcha/vue3-hcaptcha": "1.2.1", "@hcaptcha/vue3-hcaptcha": "1.2.1",
"@mdi/font": "7.0.96", "@mdi/font": "7.0.96",
"@smithy/signature-v4": "2.0.1", "@smithy/signature-v4": "2.0.1",
"@stripe/stripe-js": "2.1.0",
"bip39-english": "2.5.0", "bip39-english": "2.5.0",
"chart.js": "4.2.1", "chart.js": "4.2.1",
"pinia": "2.0.23", "pinia": "2.0.23",
"pretty-bytes": "5.6.0", "pretty-bytes": "5.6.0",
"qrcode": "1.5.3", "qrcode": "1.5.3",
"stream-browserify": "3.0.0", "stream-browserify": "3.0.0",
"stripe": "8.215.0",
"util": "0.12.5", "util": "0.12.5",
"vue": "3.3.2", "vue": "3.3.2",
"vue-datepicker-next": "1.0.3", "vue-datepicker-next": "1.0.3",

View File

@ -14,40 +14,36 @@
<script setup lang="ts"> <script setup lang="ts">
import { onBeforeUnmount, onMounted, ref } from 'vue'; import { onBeforeUnmount, onMounted, ref } from 'vue';
import { loadStripe } from '@stripe/stripe-js/pure';
import {
Stripe,
StripeCardElement,
StripeCardElementChangeEvent,
TokenResult,
} from '@stripe/stripe-js';
import { LoadScript } from '@/utils/loadScript';
import { AnalyticsErrorEventSource } from '@/utils/constants/analyticsEventNames'; import { AnalyticsErrorEventSource } from '@/utils/constants/analyticsEventNames';
import { useNotify } from '@/utils/hooks'; import { useNotify } from '@/utils/hooks';
import { useConfigStore } from '@/store/modules/configStore'; import { useConfigStore } from '@/store/modules/configStore';
interface StripeResponse {
error: string
token: {
id: unknown
card: {
funding : string
}
}
}
const configStore = useConfigStore(); const configStore = useConfigStore();
const notify = useNotify(); const notify = useNotify();
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
onStripeResponseCallback: (tokenId: unknown) => void, onStripeResponseCallback: (tokenId: unknown) => Promise<void>,
}>(), { }>(), {
onStripeResponseCallback: () => console.error('onStripeResponse is not reinitialized'), onStripeResponseCallback: () => Promise.reject('onStripeResponse is not reinitialized'),
}); });
const isLoading = ref<boolean>(false); const isLoading = ref<boolean>(false);
/** /**
* Stripe elements is using to create 'Add Card' form. * Stripe elements is used to create 'Add Card' form.
*/ */
const cardElement = ref<any>(); // eslint-disable-line @typescript-eslint/no-explicit-any const cardElement = ref<StripeCardElement>();
/** /**
* Stripe library. * Stripe library.
*/ */
const stripe = ref<any>(); // eslint-disable-line @typescript-eslint/no-explicit-any const stripe = ref<Stripe | null>(null);
/** /**
* Stripe initialization. * Stripe initialization.
@ -55,26 +51,32 @@ const stripe = ref<any>(); // eslint-disable-line @typescript-eslint/no-explicit
async function initStripe(): Promise<void> { async function initStripe(): Promise<void> {
const stripePublicKey = configStore.state.config.stripePublicKey; const stripePublicKey = configStore.state.config.stripePublicKey;
stripe.value = window['Stripe'](stripePublicKey); try {
if (!stripe.value) { stripe.value = await loadStripe(stripePublicKey);
await notify.error('Unable to initialize stripe', AnalyticsErrorEventSource.BILLING_STRIPE_CARD_INPUT); } catch (error) {
notify.error(error.message, AnalyticsErrorEventSource.BILLING_STRIPE_CARD_INPUT);
return; return;
} }
const elements = stripe.value.elements(); if (!stripe.value) {
notify.error('Unable to initialize stripe', AnalyticsErrorEventSource.BILLING_STRIPE_CARD_INPUT);
return;
}
const elements = stripe.value?.elements();
if (!elements) { if (!elements) {
await notify.error('Unable to instantiate elements', AnalyticsErrorEventSource.BILLING_STRIPE_CARD_INPUT); notify.error('Unable to instantiate elements', AnalyticsErrorEventSource.BILLING_STRIPE_CARD_INPUT);
return; return;
} }
cardElement.value = elements.create('card'); cardElement.value = elements.create('card');
if (!cardElement.value) { if (!cardElement.value) {
await notify.error('Unable to create card', AnalyticsErrorEventSource.BILLING_STRIPE_CARD_INPUT); notify.error('Unable to create card element', AnalyticsErrorEventSource.BILLING_STRIPE_CARD_INPUT);
return; return;
} }
cardElement.value.mount('#card-element'); cardElement.value?.mount('#card-element');
cardElement.value.addEventListener('change', function (event): void { cardElement.value?.on('change', (event: StripeCardElementChangeEvent) => {
const displayError: HTMLElement = document.getElementById('card-errors') as HTMLElement; const displayError: HTMLElement = document.getElementById('card-errors') as HTMLElement;
if (event.error) { if (event.error) {
displayError.textContent = event.error.message; displayError.textContent = event.error.message;
@ -90,24 +92,29 @@ async function initStripe(): Promise<void> {
* *
* @param result stripe response * @param result stripe response
*/ */
async function onStripeResponse(result: StripeResponse): Promise<void> { async function onStripeResponse(result: TokenResult): Promise<void> {
if (result.error) { if (result.error) {
throw result.error; throw result.error;
} }
if (result.token.card.funding === 'prepaid') { if (result.token.card?.funding === 'prepaid') {
notify.error('Prepaid cards are not supported', AnalyticsErrorEventSource.BILLING_STRIPE_CARD_INPUT); notify.error('Prepaid cards are not supported', AnalyticsErrorEventSource.BILLING_STRIPE_CARD_INPUT);
return; return;
} }
await props.onStripeResponseCallback(result.token.id); await props.onStripeResponseCallback(result.token.id);
cardElement.value.clear(); cardElement.value?.clear();
} }
/** /**
* Fires stripe event after all inputs are filled. * Fires stripe event after all inputs are filled.
*/ */
async function onSubmit(): Promise<void> { async function onSubmit(): Promise<void> {
if (!(stripe.value && cardElement.value)) {
notify.error('Stripe is not initialized', AnalyticsErrorEventSource.BILLING_STRIPE_CARD_INPUT);
return;
}
if (isLoading.value) return; if (isLoading.value) return;
isLoading.value = true; isLoading.value = true;
@ -124,18 +131,7 @@ async function onSubmit(): Promise<void> {
/** /**
* Stripe library loading and initialization. * Stripe library loading and initialization.
*/ */
onMounted(async (): Promise<void> => { onMounted(() => {
if (!window['Stripe']) {
const script = new LoadScript('https://js.stripe.com/v3/',
() => { initStripe(); },
() => { notify.error('Stripe library not loaded', AnalyticsErrorEventSource.BILLING_STRIPE_CARD_INPUT);
script.remove();
},
);
return;
}
initStripe(); initStripe();
}); });
@ -143,7 +139,7 @@ onMounted(async (): Promise<void> => {
* Clears listeners. * Clears listeners.
*/ */
onBeforeUnmount(() => { onBeforeUnmount(() => {
cardElement.value?.removeEventListener('change'); cardElement.value?.off('change');
}); });
defineExpose({ defineExpose({
@ -178,4 +174,10 @@ defineExpose({
.form-row { .form-row {
width: 100%; width: 100%;
} }
#card-errors {
text-align: left;
font-family: 'font-medium', sans-serif;
color: var(--c-red-2);
}
</style> </style>

View File

@ -1,55 +0,0 @@
// Copyright (C) 2020 Storj Labs, Inc.
// See LICENSE for copying information.
/**
* LoadScript is an utility for loading scripts.
*/
export class LoadScript {
public readonly head : HTMLHeadElement = document.head;
public readonly script: HTMLScriptElement = document.createElement('script');
/**
* Create script element with some predefined attributes, appends it to a DOM and start loading script.
* @param url - script url.
* @param onSuccess - this callback will be fired when load finished.
* @param onError - this callback will be fired when error occurred.
*/
public constructor(url: string, onSuccess: LoadScriptOnSuccessCallback, onError: LoadScriptOnErrorCallback) {
this.head = document.head;
this.script = document.createElement('script');
this.script.type = 'text/javascript';
this.script.charset = 'utf8';
this.script.async = true;
this.script.src = url;
this.script.onload = () => {
this.script.onerror = null;
onSuccess();
};
this.script.onerror = () => {
this.script.onerror = null;
onError(new Error('Failed to load ' + this.script.src));
};
this.head.appendChild(this.script);
}
/**
* Removes script element from DOM.
*/
public remove(): void {
this.head.removeChild(this.script);
}
}
/**
* LoadScriptOnSuccessCallback describes signature of onSuccess callback.
*/
export type LoadScriptOnSuccessCallback = () => void;
/**
* LoadScriptOnErrorCallback describes signature of onError callback.
* @param err - error occurred during script loading.
*/
export type LoadScriptOnErrorCallback = (err: Error) => void;