jcloud/dashboard/src2/components/StripeCard.vue
2025-04-12 17:39:38 +08:00

346 lines
9.4 KiB
Vue

<template>
<div class="relative">
<div
v-if="!ready"
class="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-8 transform"
>
<Spinner class="h-5 w-5 text-gray-600" />
</div>
<div :class="{ 'opacity-0': !ready }">
<div v-show="!tryingMicroCharge">
<label class="block">
<span class="block text-xs text-gray-600">
信用卡或借记卡
</span>
<div
class="form-input mt-2 block h-[unset] w-full py-2 pl-3"
ref="card-element"
></div>
<ErrorMessage class="mt-1" :message="cardErrorMessage" />
</label>
<FormControl
class="mt-4"
label="持卡人姓名"
type="text"
v-model="billingInformation.cardHolderName"
/>
<AddressForm
v-if="!withoutAddress"
class="mt-4"
v-model:address="billingInformation"
ref="address-form"
/>
</div>
<div class="mt-3 space-y-4" v-show="tryingMicroCharge">
<p class="text-base text-gray-700">
我们正在尝试向您的卡收取
<strong>{{ formattedMicroChargeAmount }}</strong> 以确保该卡有效此金额将<strong>退还</strong>到您的账户
</p>
<Button
:loading="!microChargeCompleted"
:loadingText="'正在验证卡片'"
>
卡片已验证
<template #prefix>
<GreenCheckIcon class="h-4 w-4" />
</template>
</Button>
</div>
<ErrorMessage class="mt-2" :message="errorMessage" />
<div class="mt-6 flex items-center justify-between">
<StripeLogo />
<Button
@click="clearForm"
v-if="showAddAnotherCardButton"
iconLeft="plus"
>
添加另一张卡
</Button>
<Button
v-else-if="!tryingMicroCharge"
variant="solid"
@click="submit"
:loading="addingCard"
>
验证并保存卡片
</Button>
</div>
</div>
</div>
</template>
<script>
import AddressForm from './AddressForm.vue';
import StripeLogo from '@/components/StripeLogo.vue';
import { loadStripe } from '@stripe/stripe-js';
import { toast } from 'vue-sonner';
export default {
name: 'StripeCard',
props: ['withoutAddress'],
emits: ['complete'],
components: {
AddressForm,
StripeLogo
},
data() {
return {
errorMessage: null,
cardErrorMessage: null,
ready: false,
setupIntent: null,
billingInformation: {
cardHolderName: '',
country: '',
gstin: ''
},
gstNotApplicable: false,
addingCard: false,
tryingMicroCharge: false,
showAddAnotherCardButton: false,
microChargeCompleted: false
};
},
async mounted() {
await this.setupStripeIntent();
},
resources: {
setupIntent() {
return {
url: 'jcloud.api.billing.get_publishable_key_and_setup_intent',
async onSuccess(data) {
//window.posthog.capture('init_client_add_card', 'fc_signup');
let { publishable_key, setup_intent } = data;
this.setupIntent = setup_intent;
this.stripe = await loadStripe(publishable_key);
this.elements = this.stripe.elements();
let theme = this.$theme;
let style = {
base: {
color: theme.colors.black,
fontFamily: theme.fontFamily.sans.join(', '),
fontSmoothing: 'antialiased',
fontSize: '13px',
'::placeholder': {
color: theme.colors.gray['400']
}
},
invalid: {
color: theme.colors.red['600'],
iconColor: theme.colors.red['600']
}
};
this.card = this.elements.create('card', {
hidePostalCode: true,
style: style,
classes: {
complete: '',
focus: 'bg-gray-100'
}
});
this.card.mount(this.$refs['card-element']);
this.card.addEventListener('change', event => {
this.cardErrorMessage = event.error?.message || null;
});
this.card.addEventListener('ready', () => {
this.ready = true;
});
}
};
},
countryList: 'jcloud.api.account.country_list',
billingAddress() {
return {
url: 'jcloud.api.account.get_billing_information',
params: {
timezone: this.browserTimezone
},
auto: true,
onSuccess(data) {
this.billingInformation.country = data?.country;
this.billingInformation.address = data?.address_line1;
this.billingInformation.city = data?.city;
this.billingInformation.state = data?.state;
this.billingInformation.postal_code = data?.pincode;
}
};
},
setupIntentSuccess() {
return {
url: 'jcloud.api.billing.setup_intent_success',
makeParams({ setupIntent }) {
return {
setup_intent: setupIntent,
address: this.withoutAddress ? null : this.billingInformation
};
}
};
},
verifyCardWithMicroCharge() {
return {
url: 'jcloud.api.billing.create_payment_intent_for_micro_debit',
makeParams({ paymentMethodName }) {
return {
payment_method_name: paymentMethodName
};
}
};
}
},
methods: {
async setupStripeIntent() {
await this.$resources.setupIntent.submit();
let { first_name, last_name = '' } = this.$team.pg.user_info;
let fullname = first_name + ' ' + last_name;
this.billingInformation.cardHolderName = fullname.trimEnd();
},
async submit() {
this.addingCard = true;
let message;
if (!this.withoutAddress) {
message = await this.$refs['address-form'].validateValues();
}
if (message) {
this.errorMessage = message;
this.addingCard = false;
return;
} else {
this.errorMessage = null;
}
const { setupIntent, error } = await this.stripe.confirmCardSetup(
this.setupIntent.client_secret,
{
payment_method: {
card: this.card,
billing_details: {
name: this.billingInformation.cardHolderName,
address: {
line1: this.billingInformation.address,
city: this.billingInformation.city,
state: this.billingInformation.state,
postal_code: this.billingInformation.postal_code,
country: this.getCountryCode(this.billingInformation.country)
}
}
}
}
);
if (error) {
this.addingCard = false;
let declineCode = error.decline_code;
let errorMessage = error.message;
if (declineCode === 'do_not_honor') {
this.errorMessage =
"您的卡被拒绝了。可能是由于余额不足或您可能已超过每日限额。请尝试使用另一张卡或联系您的银行。";
this.showAddAnotherCardButton = true;
} else if (declineCode === 'transaction_not_allowed') {
this.errorMessage =
'您的卡被拒绝了。可能是由于您的卡的限制,如国际交易或在线支付。请尝试使用另一张卡或联系您的银行。';
this.showAddAnotherCardButton = true;
}
// 修复重复错误消息
else if (errorMessage != '您的卡号不完整。') {
this.errorMessage = errorMessage;
}
} else {
if (setupIntent?.status === 'succeeded') {
this.$resources.setupIntentSuccess.submit(
{
setupIntent
},
{
onSuccess: async ({ payment_method_name }) => {
await this.verifyWithMicroChargeIfApplicable(
payment_method_name
);
this.addingCard = false;
toast.success('卡片添加成功');
},
onError: error => {
console.error(error);
this.addingCard = false;
this.errorMessage = error.messages.join('\n');
toast.error(this.errorMessage);
}
}
);
}
}
},
async verifyWithMicroChargeIfApplicable(paymentMethodName) {
const teamCurrency = this.$team.pg.currency;
const verifyCardsWithMicroCharge = window.verify_cards_with_micro_charge;
const isMicroChargeApplicable =
verifyCardsWithMicroCharge === 'Both CNY and USD' ||
(verifyCardsWithMicroCharge == 'Only CNY' && teamCurrency === 'CNY') ||
(verifyCardsWithMicroCharge === 'Only USD' && teamCurrency === 'USD');
if (isMicroChargeApplicable) {
await this._verifyWithMicroCharge(paymentMethodName);
} else {
this.$emit('complete');
}
},
_verifyWithMicroCharge(paymentMethodName) {
this.tryingMicroCharge = true;
return this.$resources.verifyCardWithMicroCharge.submit(
{ paymentMethodName },
{
onSuccess: async paymentIntent => {
let { client_secret } = paymentIntent;
let payload = await this.stripe.confirmCardPayment(client_secret, {
payment_method: { card: this.card }
});
if (payload.paymentIntent?.status === 'succeeded') {
this.microChargeCompleted = true;
this.$emit('complete');
}
},
onError: error => {
console.error(error);
this.tryingMicroCharge = false;
this.errorMessage = error.messages.join('\n');
}
}
);
},
getCountryCode(country) {
let code = this.$resources.countryList.data.find(
d => d.name === country
).code;
return code.toUpperCase();
},
async clearForm() {
this.ready = false;
this.errorMessage = null;
this.showAddAnotherCardButton = false;
this.card = null;
this.setupStripeIntent();
}
},
computed: {
formattedMicroChargeAmount() {
return this.$format.userCurrency(
this.$team.pg.billing_info.micro_debit_charge_amount
);
},
browserTimezone() {
if (!window.Intl) {
return null;
}
return Intl.DateTimeFormat().resolvedOptions().timeZone;
}
}
};
</script>