import { computed, reactive, readonly } from 'vue' import { useI18n } from '#imports' import { opnFetch } from '~/composables/useOpnApi.js' /** * Creates a Stripe elements instance with state management * @param {String} initialAccountId - Optional account ID to initialize with * @returns {Object} Stripe elements API with state and methods */ export const createStripeElements = (initialAccountId = null) => { // Ensure we're on the client side if (import.meta.server) { console.warn('Stripe Elements can only be initialized in the browser') return null } // Initialize i18n at the top level 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: initialAccountId, // Initialize with provided account ID intentId: null, showPreviewMessage: false, error: null }) // Reset state on initialization state.stripe = null state.elements = null state.card = null state.isStripeInstanceReady = false state.isCardElementReady = false state.error = null // 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.stripeAccountId = null state.intentId = null state.showPreviewMessage = false state.error = null } /** * Prepares the Stripe state by fetching account details * @param {String} formSlug - Form slug * @param {String|Number} providerId - OAuth provider ID * @param {Boolean} isEditorPreview - Whether this is in editor preview mode * @returns {Promise} Result object with success and message */ const prepareStripeState = async (formSlug, providerId, isEditorPreview = false) => { if (!formSlug || !providerId) { resetStripeState() return { success: false, message: 'Missing form slug or OAuth provider ID' } } // Always ensure provider ID is a string const providerIdStr = String(providerId) resetStripeState() state.isLoadingAccount = true try { console.debug('[useStripeElements] Preparing Stripe state for:', { formSlug, providerId: providerIdStr, isEditorPreview }) // Construct fetch options, adding providerId only for editor preview const fetchOptions = {} if (isEditorPreview) { fetchOptions.query = { oauth_provider_id: providerIdStr } } const response = await opnFetch(`/forms/${formSlug}/stripe-connect/get-account`, fetchOptions) console.debug('[useStripeElements] Got account response:', response) if (response?.type === 'success' && response?.stripeAccount) { // Ensure account ID is stored as string state.stripeAccountId = String(response.stripeAccount) state.isLoadingAccount = false // If card is already set, mark card element as ready if (state.card && state.stripe) { state.isCardElementReady = true } return { success: true, accountId: String(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 || 'Failed to get account details' return { success: false, message: state.errorMessage, requiresSave: state.showPreviewMessage } } } catch (error) { console.error('[useStripeElements] Error preparing state:', error) state.hasAccountLoadingError = true state.isLoadingAccount = false const message = error?.data?.message || 'Payment setup error' 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 */ const setStripeInstance = (instance) => { console.debug('[useStripeElements] Setting Stripe instance:', { hasInstance: !!instance }) try { if (!instance) { console.warn('[useStripeElements] No Stripe instance provided') return } const isValidStripeInstance = instance && typeof instance === 'object' && typeof instance.confirmCardPayment === 'function' && typeof instance.createToken === 'function' if (isValidStripeInstance) { console.debug('[useStripeElements] Valid Stripe instance detected') state.stripe = instance state.isStripeInstanceReady = true // If we have all required components, ensure card element is ready if (state.card && state.stripeAccountId) { console.debug('[useStripeElements] Card element ready with account') state.isCardElementReady = true } } else { console.warn('[useStripeElements] Invalid Stripe instance provided') state.isStripeInstanceReady = false } } catch (error) { console.error('[useStripeElements] Error setting Stripe instance:', error) } } /** * Sets the Elements instance in the state */ const setElementsInstance = (elementsInstance) => { if (elementsInstance) { state.elements = elementsInstance } } /** * Sets the Card Element in the state */ 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 */ const setBillingDetails = ({ name, email }) => { if (name !== undefined) state.cardHolderName = name if (email !== undefined) state.cardHolderEmail = email } /** * Processes a payment using the Stripe API */ const processPayment = async (formSlug, isRequired = true) => { if (!isReadyForPayment.value) { return { success: false, error: { message: t('forms.payment.errors.systemNotReady') } } } if (!state.stripe) { return { success: false, error: { message: t('forms.payment.errors.misconfigured') } } } if (!state.card) { return { success: false, error: { message: t('forms.payment.errors.notFullyReady') } } } if (isRequired && state.card._empty) { return { success: false, error: { message: t('forms.payment.errors.paymentRequired') } } } if (isRequired || !state.card._empty) { if (!state.cardHolderName) { return { success: false, error: { message: t('forms.payment.errors.nameRequired') } } } if (!state.cardHolderEmail) { return { success: false, error: { message: t('forms.payment.errors.emailRequired') } } } if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(state.cardHolderEmail)) { return { success: false, error: { message: t('forms.payment.errors.invalidEmail') } } } } try { const responseIntent = await opnFetch('/forms/' + formSlug + '/stripe-connect/payment-intent') if (responseIntent?.type === 'success') { const intentSecret = responseIntent?.intent?.secret const result = await state.stripe.confirmCardPayment(intentSecret, { payment_method: { card: state.card, billing_details: { name: state.cardHolderName, email: state.cardHolderEmail }, }, receipt_email: state.cardHolderEmail, }) 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) { 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 } } } } /** * Sets the intent ID * @param {String} intentId - The payment intent ID */ const setIntentId = (intentId) => { console.debug('[useStripeElements] Setting intentId:', intentId) if (intentId && typeof intentId === 'string' && intentId.startsWith('pi_')) { state.intentId = intentId } } /** * Sets the Stripe Account ID in the state * @param {String|Number} accountId - The Stripe account ID */ const setAccountId = (accountId) => { if (accountId) { // Always convert to string - Stripe.js requires a string account ID const accountIdStr = String(accountId) console.debug('[useStripeElements] Setting Stripe account ID:', accountIdStr) state.stripeAccountId = accountIdStr // If we have all required components, ensure card element is ready if (state.card && state.stripe) { state.isCardElementReady = true } } } const stripeElements = { // Expose readonly state to prevent mutations outside of proper methods state: readonly(state), // Read-only computed values isReadyForPayment, isCardPopulated, // Methods resetStripeState, prepareStripeState, processPayment, setStripeInstance, setElementsInstance, setCardElement, setBillingDetails, setIntentId, setAccountId } return stripeElements }