process payment fields

This commit is contained in:
Pete Matsyburka
2023-12-14 21:22:26 +02:00
parent af4cf7257f
commit 0e9343d9e2
25 changed files with 561 additions and 125 deletions
+8 -4
View File
@@ -53,7 +53,7 @@
:src="initials.url"
>
<div
v-else-if="field.type === 'file'"
v-else-if="field.type === 'file' || field.type === 'payment'"
class="px-0.5 flex flex-col justify-center"
>
<a
@@ -158,7 +158,7 @@
</template>
<script>
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconCheck, IconColumns3, IconPhoneCheck, IconLetterCaseUpper } from '@tabler/icons-vue'
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconCheck, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard } from '@tabler/icons-vue'
export default {
name: 'FieldArea',
@@ -231,7 +231,8 @@ export default {
checkbox: 'Checkbox',
radio: 'Radio',
multiple: 'Multiple Select',
phone: 'Phone'
phone: 'Phone',
payment: 'Payment'
}
},
fieldIcons () {
@@ -247,7 +248,8 @@ export default {
radio: IconCircleDot,
cells: IconColumns3,
multiple: IconChecks,
phone: IconPhoneCheck
phone: IconPhoneCheck,
payment: IconCreditCard
}
},
image () {
@@ -281,6 +283,8 @@ export default {
attachments () {
if (this.field.type === 'file') {
return (this.modelValue || []).map((uuid) => this.attachmentsIndex[uuid])
} else if (this.field.type === 'payment') {
return [this.attachmentsIndex[this.modelValue]].filter(Boolean)
} else {
return []
}
+35 -12
View File
@@ -266,8 +266,22 @@
@focus="$refs.areas.scrollIntoField(currentField)"
@submit="submitStep"
/>
<PaymentStep
v-else-if="currentField.type === 'payment'"
ref="currentStep"
:key="currentField.uuid"
v-model="values[currentField.uuid]"
:field="currentField"
:submitter-slug="submitterSlug"
@attached="attachments.push($event)"
@focus="$refs.areas.scrollIntoField(currentField)"
@submit="submitStep"
/>
</div>
<div class="mt-6 md:mt-8">
<div
v-if="currentField.type !== 'payment' || submittedValues[currentField.uuid]"
class="mt-6 md:mt-8"
>
<button
ref="submitButton"
type="submit"
@@ -331,6 +345,7 @@ import InitialsStep from './initials_step'
import AttachmentStep from './attachment_step'
import MultiSelectStep from './multi_select_step'
import PhoneStep from './phone_step'
import PaymentStep from './payment_step'
import TextStep from './text_step'
import DateStep from './date_step'
import FormCompleted from './completed'
@@ -351,6 +366,7 @@ export default {
IconArrowsDiagonal,
TextStep,
PhoneStep,
PaymentStep,
IconArrowsDiagonalMinimize2,
FormCompleted
},
@@ -402,11 +418,6 @@ export default {
required: false,
default: () => []
},
authenticityToken: {
type: String,
required: false,
default: ''
},
backgroundColor: {
type: String,
required: false,
@@ -463,6 +474,12 @@ export default {
currentStepFields () {
return this.stepFields[this.currentStep]
},
queryParams () {
return new URLSearchParams(window.location.search)
},
authenticityToken () {
return document.querySelector('meta[name="csrf-token"]')?.content
},
submitterSlug () {
return this.submitter.slug
},
@@ -525,7 +542,13 @@ export default {
}
})
if (this.goToLast) {
if (this.queryParams.get('field_uuid')) {
const stepIndex = this.stepFields.findIndex((fields) => {
return fields.some((f) => f.uuid === this.queryParams.get('field_uuid'))
})
this.currentStep = Math.max(stepIndex, 0)
} else if (this.goToLast) {
const requiredEmptyStepIndex = this.stepFields.indexOf(this.stepFields.find((fields) => fields.some((f) => f.required && !this.submittedValues[f.uuid])))
const lastFilledStepIndex = this.stepFields.indexOf([...this.stepFields].reverse().find((fields) => fields.some((f) => !!this.submittedValues[f.uuid]))) + 1
@@ -570,7 +593,7 @@ export default {
methods: {
t,
maybeTrackEmailClick () {
const queryParams = new URLSearchParams(window.location.search)
const { queryParams } = this
if (queryParams.has('t')) {
const t = queryParams.get('t')
@@ -594,7 +617,7 @@ export default {
}
},
maybeTrackSmsClick () {
const queryParams = new URLSearchParams(window.location.search)
const { queryParams } = this
if (queryParams.has('c')) {
const c = queryParams.get('c')
@@ -667,7 +690,7 @@ export default {
async submitStep () {
this.isSubmitting = true
const stepPromise = ['signature', 'phone', 'initials'].includes(this.currentField.type)
const stepPromise = ['signature', 'phone', 'initials', 'payment'].includes(this.currentField.type)
? this.$refs.currentStep.submit
: () => Promise.resolve({})
@@ -684,7 +707,7 @@ export default {
}
await this.saveStep(formData).then(async (response) => {
if (response.status === 422) {
if (response.status === 422 || response.status === 500) {
const data = await response.json()
alert(data.error || 'Value is invalid')
@@ -714,7 +737,7 @@ export default {
}
}
}).catch(error => {
console.error('Error submitting form:', error)
alert(error)
}).finally(() => {
this.isSubmitting = false
})
+27 -9
View File
@@ -38,7 +38,9 @@ const en = {
type: 'Type',
type_text: 'Type text',
date: 'Date',
email_has_been_sent: 'Email has been sent'
email_has_been_sent: 'Email has been sent',
processing: 'Processing',
pay_with_strip: 'Pay with Stripe'
}
const es = {
@@ -81,7 +83,9 @@ const es = {
draw: 'Dibujar',
type: 'Escribir',
type_text: 'Escribir texto',
email_has_been_sent: 'El correo electrónico ha sido enviado'
email_has_been_sent: 'El correo electrónico ha sido enviado',
processing: 'Procesando',
pay_with_strip: 'Pagar con Stripe'
}
const it = {
@@ -124,7 +128,9 @@ const it = {
type: 'Inserisci',
type_text: 'Inserisci testo',
toggle_multiline_text: 'Attiva Testo Multilinea',
email_has_been_sent: "L'email è stata inviata"
email_has_been_sent: "L'email è stata inviata",
processing: 'Elaborazione',
pay_with_strip: 'Paga con Stripe'
}
const de = {
@@ -167,7 +173,9 @@ const de = {
type: 'Eingeben',
type_text: 'Text eingeben',
toggle_multiline_text: 'Mehrzeiligen Text umschalten',
email_has_been_sent: 'Die E-Mail wurde gesendet'
email_has_been_sent: 'Die E-Mail wurde gesendet',
processing: 'Verarbeitung',
pay_with_strip: 'Mit Stripe bezahlen'
}
const fr = {
@@ -210,7 +218,9 @@ const fr = {
type: 'Saisir',
type_text: 'Saisir du texte',
toggle_multiline_text: 'Basculer le Texte Multiligne',
email_has_been_sent: "L'email a été envoyé"
email_has_been_sent: "L'email a été envoyé",
processing: 'Traitement',
pay_with_strip: 'Paiement avec Stripe'
}
const pl = {
@@ -253,7 +263,9 @@ const pl = {
type: 'Wprowadź',
type_text: 'Wprowadź tekst',
toggle_multiline_text: 'Przełącz Tekst Wielolinijkowy',
email_has_been_sent: 'E-mail został wysłany'
email_has_been_sent: 'E-mail został wysłany',
processing: 'Przetwarzanie',
pay_with_strip: 'Płatność za pomocą Stripe'
}
const uk = {
@@ -296,7 +308,9 @@ const uk = {
type: 'Текст',
type_text: 'Введіть текст',
toggle_multiline_text: 'Перемкнути Багаторядковий Текст',
email_has_been_sent: 'Електронний лист був відправлений'
email_has_been_sent: 'Електронний лист був відправлений',
processing: 'Обробка',
pay_with_strip: 'Сплатити за допомогою Stripe'
}
const cs = {
@@ -339,7 +353,9 @@ const cs = {
type: 'Zadat',
type_text: 'Zadat text',
toggle_multiline_text: 'Přepnout Víceřádkový Text',
email_has_been_sent: 'E-mail byl odeslán'
email_has_been_sent: 'E-mail byl odeslán',
processing: 'Zpracování',
pay_with_strip: 'Zaplacení přes Stripe'
}
const pt = {
@@ -382,7 +398,9 @@ const pt = {
type: 'Inserir',
type_text: 'Inserir texto',
toggle_multiline_text: 'Alternar Texto Multilinha',
email_has_been_sent: 'Email enviado'
email_has_been_sent: 'Email enviado',
processing: 'Processamento',
pay_with_strip: 'Pagar com Stripe'
}
const i18n = { en, es, it, de, fr, pl, uk, cs, pt }
@@ -0,0 +1,181 @@
<template>
<label
v-if="!modelValue && !sessionId"
:for="field.uuid"
class="label text-2xl mb-2"
>{{ field.name || defaultName }}
</label>
<div>
<input
type="text"
:value="modelValue"
hidden
:name="`values[${field.uuid}]`"
class="hidden"
>
<div
v-if="modelValue && !sessionId"
class=" text-2xl mb-2"
>
Already paid
</div>
<div v-else>
<button
v-if="sessionId"
disabled
class="base-button w-full"
>
<IconLoader
width="22"
class="animate-spin"
/>
<span>
{{ t('processing') }}...
</span>
</button>
<button
v-else
:id="field.uuid"
class="btn bg-[#7B73FF] text-white hover:bg-[#0A2540] text-lg w-full"
:class="{ disabled: isCreatingCheckout }"
:disabled="isCreatingCheckout"
@click.prevent="startCheckout"
>
<IconInnerShadowTop
v-if="isCreatingCheckout"
width="22"
class="animate-spin"
/>
<IconBrandStripe
v-else
width="22"
/>
<span>
{{ t('pay_with_strip') }}
</span>
</button>
</div>
</div>
</template>
<script>
import { IconBrandStripe, IconInnerShadowTop, IconLoader } from '@tabler/icons-vue'
export default {
name: 'PaymentStep',
components: {
IconBrandStripe,
IconInnerShadowTop,
IconLoader
},
inject: ['baseUrl', 't'],
props: {
modelValue: {
type: String,
required: false,
default: ''
},
field: {
type: Object,
required: true
},
submitterSlug: {
type: String,
required: true
}
},
emits: ['focus', 'submit', 'update:model-value', 'attached'],
data () {
return {
isCreatingCheckout: false
}
},
computed: {
queryParams () {
return new URLSearchParams(window.location.search)
},
sessionId () {
return this.queryParams.get('stripe_session_id')
},
defaultName () {
const { price, currency } = this.field.preferences || {}
const formattedPrice = new Intl.NumberFormat([], {
style: 'currency',
currency
}).format(price)
return `Pay ${formattedPrice}`
}
},
mounted () {
if (this.sessionId) {
this.$emit('submit')
}
},
methods: {
async submit () {
if (this.sessionId) {
return fetch((this.baseUrl || '/embed').replace('/embed', '/api/stripe_payments/' + this.sessionId), {
method: 'PUT',
body: JSON.stringify({
submitter_slug: this.submitterSlug
}),
headers: { 'Content-Type': 'application/json' }
}).then(async (resp) => {
if (resp.status === 422 || resp.status === 500) {
const data = await resp.json()
alert(data.error || 'Unexpected error')
return Promise.reject(new Error(data.error))
}
const attachment = await resp.json()
window.history.replaceState({}, document.title, window.location.pathname)
this.$emit('update:model-value', attachment.uuid)
this.$emit('attached', attachment)
return resp
})
} else {
return Promise.resolve({})
}
},
startCheckout () {
this.isCreatingCheckout = true
fetch((this.baseUrl || '/embed').replace('/embed', '/api/stripe_payments'), {
method: 'POST',
body: JSON.stringify({
submitter_slug: this.submitterSlug,
field_uuid: this.field.uuid
}),
headers: { 'Content-Type': 'application/json' }
}).then(async (resp) => {
if (resp.status === 422 || resp.status === 500) {
const data = await resp.json()
alert(data.message || 'Unexpected error')
return Promise.reject(new Error(data.message))
}
const { url } = await resp.json()
const link = document.createElement('a')
link.href = url
if (url) {
link.click()
}
}).finally(() => {
this.isCreatingCheckout = false
})
}
}
}
</script>