import { toValue, ref } from 'vue' import { createStripeElements } from '~/composables/useStripeElements' import { opnFetch } from '~/composables/useOpnApi.js' // Assume Stripe is loaded globally or via another mechanism if needed client-side // For server-side/Nuxt API routes, import Stripe library properly. /** * @fileoverview Composable for handling payment processing, currently focused on Stripe. */ export function useFormPayment(formConfig, form) { const stripeElements = ref(null) /** * Gets payment-related data for a specific payment block * @param {Object} paymentBlock - The payment field configuration * @returns {Object|null} Payment data including stripeElements or null if not applicable */ const getPaymentData = (paymentBlock) => { if (!import.meta.client || !paymentBlock || paymentBlock.type !== 'payment') return null // Create Stripe elements if needed and this is a Stripe payment if (paymentBlock.provider === 'stripe' || !paymentBlock.provider) { // Ensure account ID is a string (Stripe.js requires a string) const accountId = paymentBlock.stripe_account_id ? String(paymentBlock.stripe_account_id) : null if (!stripeElements.value) { // Create the Stripe elements with the account ID stripeElements.value = createStripeElements(accountId) } else if (stripeElements.value && accountId) { // Update the account ID if the instance already exists // Use the proper setter method to avoid readonly errors if (typeof stripeElements.value.setAccountId === 'function') { stripeElements.value.setAccountId(accountId) } } return { stripeElements: stripeElements.value, oauthProviderId: accountId } } return null } /** * Creates a payment intent with the Stripe API using a POST request. * @param {Number} amount - The amount to charge in cents. * @param {String} currency - The currency code (e.g., 'usd'). * @param {String} description - A description for the payment. * @returns {Promise} The result of creating the payment intent. */ const _createPaymentIntent = async (_amount, _currency, _description) => { if (!import.meta.client) { return { success: false, error: 'Client-side only operation' } } try { // Get form slug from config const config = toValue(formConfig) const formSlug = config.slug if (!formSlug) { console.error('Missing form slug in config') return { success: false, error: 'Invalid form configuration' } } // Construct the URL (no query params needed for POST) const url = `/forms/${formSlug}/stripe-connect/payment-intent` // Use opnFetch with POST method and an empty body const response = await opnFetch(url, { method: 'POST', body: {} }) // Handle response structure with type and intent fields if (response?.type === 'success' && response?.intent?.secret) { return { success: true, client_secret: response.intent.secret, intentId: response.intent.id } } // Handle error response return { success: false, error: response?.message || 'Could not create payment: Invalid response from server' } } catch (error) { return { success: false, error: error.message || 'Failed to create payment' } } } /** * Confirms the Stripe payment on the client-side using Stripe.js. * @param {String} clientSecret - The Stripe client secret. * @param {String} paymentBlockId - The ID of the payment block for setting errors. * @returns {Promise} The result of the payment confirmation. */ const _confirmStripePayment = async (clientSecret, paymentBlockId) => { if (!import.meta.client) { return { success: false, error: 'Client-side only operation' } } if (!stripeElements.value || !stripeElements.value.state) { return { success: false, error: 'Stripe elements not initialized' } } const state = stripeElements.value.state const { stripe, card } = state if (!stripe || !card) { const error = 'Stripe or card element not available' if (paymentBlockId) form.errors.set(paymentBlockId, error) return { success: false, error } } try { const result = await stripe.confirmCardPayment(clientSecret, { payment_method: { card: card, billing_details: { name: state.cardHolderName || '', email: state.cardHolderEmail || '' } }, receipt_email: state.cardHolderEmail }) // Check for errors if (result.error) { console.error('Payment confirmation error:', result.error) const errorMessage = result.error.message || 'Payment failed. Please try again.' if (paymentBlockId) { form.errors.set(paymentBlockId, errorMessage) } return { success: false, error: errorMessage, code: result.error.code, type: result.error.type } } // Handle payment intent status if (result.paymentIntent) { const status = result.paymentIntent.status const intentId = result.paymentIntent.id // Store successful payment intent ID if (status === 'succeeded' || status === 'processing') { // Update form data with payment information const updateData = {} updateData['stripe_payment_intent_id'] = intentId updateData['payment_status'] = status // Update payment block field with intent ID if (paymentBlockId) { updateData[paymentBlockId] = intentId } // Update form data form.update(updateData) // Also update the Stripe state if (state.intentId !== intentId && stripeElements.value.setIntentId) { stripeElements.value.setIntentId(intentId) } return { success: true, paymentIntent: result.paymentIntent, status: status, intentId: intentId } } else { // Payment intent exists but status is not successful const failMessage = `Payment failed with status: ${status}` console.error(failMessage) if (paymentBlockId) { form.errors.set(paymentBlockId, failMessage) } return { success: false, error: failMessage } } } // If we get here, something unexpected happened const unexpectedError = 'Payment failed with an unexpected error' if (paymentBlockId) { form.errors.set(paymentBlockId, unexpectedError) } return { success: false, error: unexpectedError } } catch (error) { console.error('Payment confirmation error:', error) const errorMessage = error.message || 'Payment confirmation failed' if (paymentBlockId) { form.errors.set(paymentBlockId, errorMessage) } return { success: false, error: errorMessage, code: error.code, type: error.type } } } /** * Process a payment for the form * @param {Object} paymentBlock - The payment block from the form config * @param {Boolean} isRequired - Whether payment is required * @returns {Promise} The result of the payment processing */ const processPayment = async (paymentBlock, isRequired = true) => { // Only process payments on the client side if (!import.meta.client) { console.warn('Payment processing attempted on server') return { success: false, error: 'Payment can only be processed in the browser' } } // Validate the payment block if (!paymentBlock || paymentBlock.type !== 'payment') { console.error('Invalid payment block provided:', paymentBlock) return { success: false, error: 'Invalid payment block' } } const paymentBlockId = paymentBlock.id // First check for existing payment in form data const existingPaymentId = form[paymentBlockId] if (existingPaymentId && isPaymentIntentId(existingPaymentId)) { return { success: true, intentId: existingPaymentId } } // Check if Stripe elements are initialized if (!stripeElements.value || !stripeElements.value.state) { console.error('Stripe elements not initialized') if (paymentBlockId) form.errors.set(paymentBlockId, 'Payment system not ready') return { success: false, error: 'Stripe elements not initialized' } } const state = stripeElements.value.state const { stripe, card } = state // Check if Stripe is loaded if (!stripe) { const error = 'Stripe.js not initialized' console.error(error) if (paymentBlockId) form.errors.set(paymentBlockId, error) return { success: false, error } } // Check if card is complete const cardComplete = card && !card._empty if (!cardComplete) { // If payment is not required and card is empty, just skip payment if (!isRequired) { return { success: true, skipped: true } } const error = 'Please enter your card details' if (paymentBlockId) form.errors.set(paymentBlockId, error) return { success: false, error } } // Validate billing details if (!state.cardHolderName) { const error = 'Please enter the name on your card' if (paymentBlockId) form.errors.set(paymentBlockId, error) return { success: false, error } } if (!state.cardHolderEmail) { const error = 'Please enter your billing email' if (paymentBlockId) form.errors.set(paymentBlockId, error) return { success: false, error } } try { // Step 1: Create payment intent const config = toValue(formConfig) const formSlug = config.slug if (!formSlug) { const error = 'Missing form slug' if (paymentBlockId) form.errors.set(paymentBlockId, error) return { success: false, error } } // Create payment intent const intentResult = await _createPaymentIntent( paymentBlock.amount, paymentBlock.currency || 'usd', paymentBlock.description || '' ) if (!intentResult.success || !intentResult.client_secret) { const error = intentResult.error || 'Failed to create payment intent' console.error('Payment intent creation failed:', error) if (paymentBlockId) form.errors.set(paymentBlockId, error) return { success: false, error } } // Step 2: Confirm payment with Stripe const confirmResult = await _confirmStripePayment(intentResult.client_secret, paymentBlockId) // Return the result from confirmation return confirmResult } catch (error) { console.error('Payment processing error:', error) const errorMessage = error.message || 'Payment processing failed' if (paymentBlockId) form.errors.set(paymentBlockId, errorMessage) return { success: false, error: errorMessage } } } /** * Confirms a Stripe payment with given client secret. * This method is available for direct usage if needed. * @param {String} clientSecret - The client secret from a payment intent * @param {String} [paymentBlockId] - Optional ID of payment block for error display * @returns {Promise} The result of payment confirmation */ const confirmStripePayment = async (clientSecret, paymentBlockId) => { if (!clientSecret) { console.error('Missing client secret for payment confirmation') return { success: false, error: 'Invalid payment data' } } return await _confirmStripePayment(clientSecret, paymentBlockId) } // Helper function to check if a value looks like a Stripe payment intent ID const isPaymentIntentId = (value) => { return typeof value === 'string' && value.startsWith('pi_') } // Expose the main payment processing function return { processPayment, getPaymentData, createPaymentIntent: _createPaymentIntent, confirmStripePayment } }