337 lines
9.7 KiB
JavaScript
337 lines
9.7 KiB
JavaScript
import { computed, provide, inject, reactive } from 'vue'
|
|
import { useI18n } from '#imports'
|
|
|
|
// Symbol for injection key
|
|
export const STRIPE_ELEMENTS_KEY = Symbol('stripe-elements')
|
|
|
|
export const createStripeElements = () => {
|
|
// Get the translation function
|
|
const { t } = useI18n()
|
|
|
|
// Use reactive for the state to ensure changes propagate
|
|
const state = reactive({
|
|
// Loading states
|
|
isLoadingAccount: false,
|
|
hasAccountLoadingError: false,
|
|
isStripeInstanceReady: false,
|
|
isCardElementReady: false,
|
|
|
|
// Core Stripe objects
|
|
stripe: null,
|
|
elements: null,
|
|
card: null,
|
|
|
|
// Form data
|
|
cardHolderName: '',
|
|
cardHolderEmail: '',
|
|
|
|
// Account & payment state
|
|
stripeAccountId: null,
|
|
intentId: null,
|
|
showPreviewMessage: false,
|
|
errorMessage: ''
|
|
})
|
|
|
|
// Computed properties
|
|
const isReadyForPayment = computed(() => {
|
|
return state.isStripeInstanceReady &&
|
|
state.isCardElementReady &&
|
|
state.stripeAccountId
|
|
})
|
|
|
|
const isCardPopulated = computed(() => {
|
|
return state.card && !state.card._empty
|
|
})
|
|
|
|
/**
|
|
* Resets the Stripe state to its initial values
|
|
*/
|
|
const resetStripeState = () => {
|
|
state.isLoadingAccount = false
|
|
state.hasAccountLoadingError = false
|
|
state.isStripeInstanceReady = false
|
|
state.isCardElementReady = false
|
|
state.stripe = null
|
|
state.elements = null
|
|
state.card = null
|
|
state.intentId = null
|
|
state.showPreviewMessage = false
|
|
state.stripeAccountId = null
|
|
state.errorMessage = ''
|
|
}
|
|
|
|
/**
|
|
* Fetches the Stripe account ID required for connecting to the proper account
|
|
* @param {string} formSlug - The slug of the form
|
|
* @param {string} providerId - The OAuth provider ID
|
|
* @param {boolean} isEditorPreview - Whether this is in editor preview mode
|
|
* @returns {Promise<Object>} - Object containing success/error information
|
|
*/
|
|
const prepareStripeState = async (formSlug, providerId, isEditorPreview = false) => {
|
|
if (!formSlug || !providerId) {
|
|
resetStripeState()
|
|
return { success: false, message: t('forms.payment.errors.missingFormOrProvider') }
|
|
}
|
|
|
|
resetStripeState()
|
|
state.isLoadingAccount = true
|
|
|
|
try {
|
|
const fetchOptions = {}
|
|
if (isEditorPreview) {
|
|
fetchOptions.query = { oauth_provider_id: providerId }
|
|
}
|
|
|
|
const response = await opnFetch(`/forms/${formSlug}/stripe-connect/get-account`, fetchOptions)
|
|
|
|
if (response?.type === 'success' && response?.stripeAccount) {
|
|
// Explicitly set the account ID in state
|
|
state.stripeAccountId = response.stripeAccount
|
|
state.isLoadingAccount = false
|
|
|
|
// We'll rely on the StripeElements component to create the Stripe instance
|
|
// Don't try to create it here
|
|
|
|
return { success: true, accountId: response.stripeAccount }
|
|
} else {
|
|
state.hasAccountLoadingError = true
|
|
state.isLoadingAccount = false
|
|
|
|
if (response?.message?.includes('save the form and try again')) {
|
|
state.showPreviewMessage = true
|
|
}
|
|
|
|
state.errorMessage = response?.message || t('forms.payment.errors.failedAccountDetails')
|
|
return {
|
|
success: false,
|
|
message: state.errorMessage,
|
|
requiresSave: state.showPreviewMessage
|
|
}
|
|
}
|
|
} catch (error) {
|
|
state.hasAccountLoadingError = true
|
|
state.isLoadingAccount = false
|
|
|
|
const message = error?.data?.message || t('forms.payment.errors.setupError')
|
|
|
|
if (message.includes('save the form and try again')) {
|
|
state.showPreviewMessage = true
|
|
}
|
|
|
|
state.errorMessage = message
|
|
return {
|
|
success: false,
|
|
message: state.errorMessage,
|
|
requiresSave: state.showPreviewMessage
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets the Stripe instance in the state
|
|
* @param {Object} instance - The Stripe instance from vue-stripe-js
|
|
*/
|
|
const setStripeInstance = (instance) => {
|
|
// Check if the instance is actually a Stripe instance by looking for known methods
|
|
const isValidStripeInstance = instance &&
|
|
typeof instance === 'object' &&
|
|
typeof instance.confirmCardPayment === 'function' &&
|
|
typeof instance.createToken === 'function'
|
|
|
|
if (instance && isValidStripeInstance) {
|
|
// Only set if the instance is different to avoid unnecessary updates
|
|
if (state.stripe !== instance) {
|
|
state.stripe = instance
|
|
state.isStripeInstanceReady = true
|
|
}
|
|
} else {
|
|
state.stripe = null
|
|
state.isStripeInstanceReady = false
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets the Elements instance in the state
|
|
* @param {Object} elementsInstance - The Elements instance from vue-stripe-js
|
|
*/
|
|
const setElementsInstance = (elementsInstance) => {
|
|
if (elementsInstance) {
|
|
state.elements = elementsInstance
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets the Card Element in the state
|
|
* @param {Object} cardElement - The Card Element instance
|
|
*/
|
|
const setCardElement = (cardElement) => {
|
|
if (cardElement) {
|
|
state.card = cardElement
|
|
state.isCardElementReady = true
|
|
} else {
|
|
state.card = null
|
|
state.isCardElementReady = false
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets the billing details in the state
|
|
* @param {Object} details - The billing details object {name, email}
|
|
*/
|
|
const setBillingDetails = ({ name, email }) => {
|
|
if (name !== undefined) state.cardHolderName = name
|
|
if (email !== undefined) state.cardHolderEmail = email
|
|
}
|
|
|
|
/**
|
|
* Processes a payment using the Stripe API
|
|
* @param {string} formSlug - The slug of the form
|
|
* @param {boolean} isRequired - Whether payment is required to proceed
|
|
* @returns {Promise<Object>} - Object containing payment result or error
|
|
*/
|
|
const processPayment = async (formSlug, isRequired = true) => {
|
|
// Check if Stripe is fully initialized
|
|
if (!isReadyForPayment.value) {
|
|
return {
|
|
success: false,
|
|
error: { message: t('forms.payment.errors.systemNotReady') }
|
|
}
|
|
}
|
|
|
|
// Check if the stripe instance exists
|
|
if (!state.stripe) {
|
|
return {
|
|
success: false,
|
|
error: { message: t('forms.payment.errors.misconfigured') }
|
|
}
|
|
}
|
|
|
|
// Additional validation for card
|
|
if (!state.card) {
|
|
return {
|
|
success: false,
|
|
error: { message: t('forms.payment.errors.notFullyReady') }
|
|
}
|
|
}
|
|
|
|
// Check if payment is required but card is empty
|
|
if (isRequired && state.card._empty) {
|
|
return {
|
|
success: false,
|
|
error: { message: t('forms.payment.errors.paymentRequired') }
|
|
}
|
|
}
|
|
|
|
// Only validate billing details if payment is required or card has data
|
|
if (isRequired || !state.card._empty) {
|
|
// Validate card holder name
|
|
if (!state.cardHolderName) {
|
|
return {
|
|
success: false,
|
|
error: { message: t('forms.payment.errors.nameRequired') }
|
|
}
|
|
}
|
|
|
|
// Validate billing email
|
|
if (!state.cardHolderEmail) {
|
|
return {
|
|
success: false,
|
|
error: { message: t('forms.payment.errors.emailRequired') }
|
|
}
|
|
}
|
|
|
|
// Validate email format
|
|
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(state.cardHolderEmail)) {
|
|
return {
|
|
success: false,
|
|
error: { message: t('forms.payment.errors.invalidEmail') }
|
|
}
|
|
}
|
|
}
|
|
|
|
try {
|
|
// Get payment intent from server
|
|
const responseIntent = await opnFetch('/forms/' + formSlug + '/stripe-connect/payment-intent')
|
|
|
|
if (responseIntent?.type === 'success') {
|
|
const intentSecret = responseIntent?.intent?.secret
|
|
|
|
// Confirm card payment with Stripe
|
|
const result = await state.stripe.confirmCardPayment(intentSecret, {
|
|
payment_method: {
|
|
card: state.card,
|
|
billing_details: {
|
|
name: state.cardHolderName,
|
|
email: state.cardHolderEmail
|
|
},
|
|
},
|
|
receipt_email: state.cardHolderEmail,
|
|
})
|
|
|
|
// Store payment intent ID on success
|
|
if (result?.paymentIntent?.status === 'succeeded') {
|
|
state.intentId = result.paymentIntent.id
|
|
}
|
|
|
|
return {
|
|
success: result?.paymentIntent?.status === 'succeeded',
|
|
...result
|
|
}
|
|
} else {
|
|
return {
|
|
success: false,
|
|
error: { message: responseIntent?.message || t('forms.payment.errors.failedIntent') }
|
|
}
|
|
}
|
|
} catch (error) {
|
|
// Include more details about the error
|
|
const errorMessage = error?.message || t('forms.payment.errors.processingFailed')
|
|
const errorType = error?.type || 'unknown'
|
|
const errorCode = error?.code || 'unknown'
|
|
|
|
return {
|
|
success: false,
|
|
error: {
|
|
message: errorMessage,
|
|
type: errorType,
|
|
code: errorCode
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const stripeElements = {
|
|
state,
|
|
isReadyForPayment,
|
|
isCardPopulated,
|
|
processPayment,
|
|
resetStripeState,
|
|
prepareStripeState,
|
|
setStripeInstance,
|
|
setElementsInstance,
|
|
setCardElement,
|
|
setBillingDetails
|
|
}
|
|
|
|
// Return the API
|
|
return stripeElements
|
|
}
|
|
|
|
// Use this in the provider component (OpenForm)
|
|
export const provideStripeElements = () => {
|
|
const stripeElements = createStripeElements()
|
|
|
|
// Provide the entire stripeElements object to ensure reactivity
|
|
provide(STRIPE_ELEMENTS_KEY, stripeElements)
|
|
|
|
return stripeElements
|
|
}
|
|
|
|
// Use this in consumer components (PaymentInput)
|
|
export const useStripeElements = () => {
|
|
const stripeElements = inject(STRIPE_ELEMENTS_KEY)
|
|
if (!stripeElements) {
|
|
console.error('stripeElements was not provided. Make sure to call provideStripeElements in a parent component')
|
|
}
|
|
return stripeElements
|
|
} |