jcloude/dashboard/src/pages/CheckoutPayment.vue
2025-12-23 20:48:07 +08:00

199 lines
4.8 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>
<label class="block">
<span class="block text-xs text-gray-600">
Credit or Debit Card
</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="Name on Card"
type="text"
v-model="cardHolderName"
/>
</div>
<ErrorMessage class="mt-2" :message="errorMessage" />
<div class="mt-6 flex items-center justify-between">
<StripeLogo />
<Button variant="solid" @click="submit" :loading="addingCard">
Save Card
</Button>
</div>
</div>
</div>
</template>
<script>
import AddressForm from '../components/AddressForm.vue';
import StripeLogo from '@/components/StripeLogo.vue';
import { loadStripe } from '@stripe/stripe-js';
import { frappeRequest } from 'jingrow-ui';
export default {
name: 'StripeCard',
props: ['secretKey', 'address', 'currency', 'selectedPlan'],
emits: ['complete'],
components: {
AddressForm,
StripeLogo,
},
data() {
return {
errorMessage: null,
cardErrorMessage: null,
ready: false,
setupIntent: null,
cardHolderName: '',
gstNotApplicable: false,
addingCard: false,
};
},
async mounted() {
this.setupCard();
},
resources: {
countryList() {
return {
url: 'jcloude.api.account.country_list',
auto: true,
};
},
},
methods: {
async setupCard() {
let result = await frappeRequest({
url: 'jcloude.api.developer.marketplace.get_publishable_key_and_setup_intent',
params: { secret_key: this.secretKey },
});
//window.posthog.capture('init_client_add_card', 'fc_signup');
let { publishable_key, setup_intent } = result;
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;
});
},
async submit() {
this.addingCard = true;
let message;
if (!this.address) {
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.cardHolderName,
address: {
line1: this.address.address_line1,
city: this.address.city,
state: this.address.state,
postal_code: this.address.postal_code,
country: this.getCountryCode(this.address.country),
},
},
},
},
);
if (error) {
this.addingCard = false;
let errorMessage = error.message;
// fix for duplicate error message
if (errorMessage != 'Your card number is incomplete.') {
this.errorMessage = errorMessage;
}
} else {
if (setupIntent.status === 'succeeded') {
try {
const { payment_method_name } = await frappeRequest({
url: 'jcloude.api.developer.marketplace.setup_intent_success',
params: {
secret_key: this.secretKey,
setup_intent: setupIntent,
},
});
this.addingCard = false;
await frappeRequest({
url: 'jcloude.api.developer.marketplace.change_site_plan',
params: {
secret_key: this.secretKey,
plan: this.selectedPlan.name,
},
});
this.$emit('update:step', 4);
} catch (error) {
console.error(error);
this.addingCard = false;
this.errorMessage = error.messages.join('\n');
}
}
}
},
getCountryCode(country) {
country = 'India';
let code = this.$resources.countryList.data.find(
(d) => d.name === country,
).code;
return code.toUpperCase();
},
},
};
</script>