Files
docuseal/app/javascript/submission_form/payment_step.vue
T

303 lines
7.3 KiB
Vue
Raw Normal View History

2023-12-14 21:22:26 +02:00
<template>
<label
2026-07-28 19:27:25 +03:00
v-if="!modelValue && !isProcessing"
2025-03-18 17:34:11 +02:00
class="label text-xl sm:text-2xl py-0 mb-2 sm:mb-3.5 field-name-label"
2024-07-07 19:55:56 +03:00
>
<MarkdownContent
v-if="field.title"
:string="field.title"
/>
<template v-else>{{ field.name || defaultName }}</template>
2023-12-14 21:22:26 +02:00
</label>
2024-07-07 19:55:56 +03:00
<div
v-if="field.description"
dir="auto"
class="mb-4 px-1"
>
<MarkdownContent :string="field.description" />
</div>
2023-12-14 21:22:26 +02:00
<div>
<input
type="text"
:value="modelValue"
hidden
:name="`values[${field.uuid}]`"
class="hidden"
>
<div
2026-07-28 19:27:25 +03:00
v-if="modelValue && !isProcessing"
2023-12-14 21:22:26 +02:00
class=" text-2xl mb-2"
>
2024-07-07 19:55:56 +03:00
{{ t('already_paid') }}
2023-12-14 21:22:26 +02:00
</div>
<div v-else>
<button
2026-07-28 19:27:25 +03:00
v-if="isProcessing"
2026-05-31 09:09:57 +03:00
type="button"
2023-12-14 21:22:26 +02:00
disabled
2025-03-18 17:34:11 +02:00
class="base-button w-full modal-save-button"
2023-12-14 21:22:26 +02:00
>
<IconLoader
width="22"
class="animate-spin"
/>
<span>
{{ t('processing') }}...
</span>
</button>
<button
v-else
:id="field.uuid"
2026-05-31 09:09:57 +03:00
type="button"
2026-07-28 19:27:25 +03:00
class="btn text-white text-lg w-full"
:class="[isPaypal ? 'bg-[#003087] hover:bg-[#012169]' : 'bg-[#7B73FF] hover:bg-[#0A2540]', { disabled: isCreatingCheckout }]"
2023-12-14 21:22:26 +02:00
:disabled="isCreatingCheckout"
2024-07-12 15:25:48 +03:00
@click.prevent="postCheckout"
2023-12-14 21:22:26 +02:00
>
<IconInnerShadowTop
v-if="isCreatingCheckout"
width="22"
class="animate-spin"
/>
2026-07-28 19:27:25 +03:00
<IconBrandPaypal
v-else-if="isPaypal"
width="22"
/>
2023-12-14 21:22:26 +02:00
<IconBrandStripe
v-else
width="22"
/>
<span>
2026-07-28 19:27:25 +03:00
{{ isPaypal ? t('pay_with_paypal') : t('pay_with_stripe') }}
2023-12-14 21:22:26 +02:00
</span>
</button>
</div>
</div>
</template>
<script>
2026-07-28 19:27:25 +03:00
import { IconBrandStripe, IconBrandPaypal, IconInnerShadowTop, IconLoader } from '@tabler/icons-vue'
2024-07-07 19:55:56 +03:00
import MarkdownContent from './markdown_content'
2023-12-14 21:22:26 +02:00
export default {
name: 'PaymentStep',
components: {
IconBrandStripe,
2026-07-28 19:27:25 +03:00
IconBrandPaypal,
2024-07-07 19:55:56 +03:00
MarkdownContent,
2023-12-14 21:22:26 +02:00
IconInnerShadowTop,
IconLoader
},
inject: ['baseUrl', 't'],
props: {
modelValue: {
type: String,
required: false,
default: ''
},
field: {
type: Object,
required: true
},
2025-09-25 13:12:36 +03:00
readonlyValues: {
type: Object,
required: false,
default: () => ({})
},
2024-07-31 22:56:18 +03:00
values: {
type: Object,
required: true
},
2025-09-25 13:12:36 +03:00
fields: {
type: Array,
required: false,
default: () => []
},
2023-12-14 21:22:26 +02:00
submitterSlug: {
type: String,
required: true
2026-07-28 19:27:25 +03:00
},
provider: {
type: String,
required: false,
default: ''
2023-12-14 21:22:26 +02:00
}
},
emits: ['focus', 'submit', 'update:model-value', 'attached'],
data () {
return {
2024-07-31 22:56:18 +03:00
isCreatingCheckout: false,
2026-07-28 19:27:25 +03:00
isProcessing: false,
2024-07-31 22:56:18 +03:00
isMathLoaded: false
2023-12-14 21:22:26 +02:00
}
},
computed: {
2025-09-25 13:12:36 +03:00
fieldsUuidIndex () {
return this.fields.reduce((acc, field) => {
acc[field.uuid] = field
return acc
}, {})
},
2023-12-14 21:22:26 +02:00
queryParams () {
return new URLSearchParams(window.location.search)
},
2026-07-28 19:27:25 +03:00
isPaypal () {
return this.provider === 'paypal'
},
paymentEndpoint () {
return this.isPaypal ? '/api/paypal_payments' : '/api/stripe_payments'
},
2023-12-14 21:22:26 +02:00
sessionId () {
2026-07-28 19:27:25 +03:00
return this.queryParams.get('stripe_session_id') || this.queryParams.get('paypal_order_id') || (this.isPaypal && this.queryParams.get('token'))
2023-12-14 21:22:26 +02:00
},
defaultName () {
2025-09-29 10:23:11 +03:00
if (this.field.preferences?.price_id || this.field.preferences?.payment_link_id) {
2025-09-23 10:02:10 +03:00
return ''
}
2023-12-14 21:22:26 +02:00
const { price, currency } = this.field.preferences || {}
2024-07-31 22:56:18 +03:00
const formatter = new Intl.NumberFormat([], {
2023-12-14 21:22:26 +02:00
style: 'currency',
currency
2024-07-31 22:56:18 +03:00
})
2023-12-14 21:22:26 +02:00
2024-07-31 22:56:18 +03:00
if (this.field.preferences?.formula) {
if (this.isMathLoaded) {
return this.t('pay') + ' ' + formatter.format(this.calculateFormula())
} else {
return ''
}
} else {
return this.t('pay') + ' ' + formatter.format(price)
}
2023-12-14 21:22:26 +02:00
}
},
2024-07-31 22:56:18 +03:00
async mounted () {
2023-12-14 21:22:26 +02:00
if (this.sessionId) {
2026-07-28 19:27:25 +03:00
this.isProcessing = true
2023-12-14 21:22:26 +02:00
this.$emit('submit')
}
2024-07-12 15:25:48 +03:00
if (!this.sessionId) {
this.postCheckout({ checkStatus: true })
}
2024-07-31 22:56:18 +03:00
if (this.field.preferences?.formula) {
2026-04-11 08:12:30 +03:00
const { Calculator } = await import('./calculator')
2024-07-31 22:56:18 +03:00
2026-04-11 08:12:30 +03:00
this.math = new Calculator()
2024-07-31 22:56:18 +03:00
this.isMathLoaded = true
}
2023-12-14 21:22:26 +02:00
},
methods: {
2026-07-28 19:27:25 +03:00
normalizedPaymentUrl () {
const url = new URL(window.location.href)
const keys = ['stripe_session_id', 'paypal_order_id', 'field_uuid']
if (this.isPaypal) {
keys.push('token', 'PayerID')
}
keys.forEach((key) => url.searchParams.delete(key))
return url.toString()
},
normalizedPaymentParams () {
window.history.replaceState({}, document.title, this.normalizedPaymentUrl())
},
2024-07-31 22:56:18 +03:00
calculateFormula () {
2025-09-25 13:12:36 +03:00
const transformedFormula = this.normalizeFormula(this.field.preferences.formula).replace(/{{(.*?)}}/g, (match, uuid) => {
return this.readonlyValues[uuid] || this.values[uuid] || 0.0
2024-07-31 22:56:18 +03:00
})
return this.math.evaluate(transformedFormula.toLowerCase())
},
2025-09-25 13:12:36 +03:00
normalizeFormula (formula, depth = 0) {
if (depth > 10) return formula
return formula.replace(/{{(.*?)}}/g, (match, uuid) => {
if (this.fieldsUuidIndex[uuid]) {
return `(${this.normalizeFormula(this.fieldsUuidIndex[uuid].preferences.formula, depth + 1)})`
} else {
return match
}
})
},
2023-12-14 21:22:26 +02:00
async submit () {
if (this.sessionId) {
2026-07-28 19:27:25 +03:00
return fetch(this.baseUrl + this.paymentEndpoint + '/' + this.sessionId, {
2023-12-14 21:22:26 +02:00
method: 'PUT',
body: JSON.stringify({
submitter_slug: this.submitterSlug
}),
headers: { 'Content-Type': 'application/json' }
}).then(async (resp) => {
if (resp.status === 422 || resp.status === 500) {
2026-07-28 19:27:25 +03:00
this.isProcessing = false
2023-12-14 21:22:26 +02:00
const data = await resp.json()
2026-07-28 19:27:25 +03:00
if (data.error) {
alert(data.error)
}
2023-12-14 21:22:26 +02:00
return Promise.reject(new Error(data.error))
}
const attachment = await resp.json()
2026-07-28 19:27:25 +03:00
this.isProcessing = false
this.normalizedPaymentParams()
2023-12-14 21:22:26 +02:00
this.$emit('update:model-value', attachment.uuid)
this.$emit('attached', attachment)
return resp
})
} else {
return Promise.resolve({})
}
},
2024-07-12 15:25:48 +03:00
postCheckout ({ checkStatus } = {}) {
2023-12-14 21:22:26 +02:00
this.isCreatingCheckout = true
2026-07-28 19:27:25 +03:00
fetch(this.baseUrl + this.paymentEndpoint, {
2023-12-14 21:22:26 +02:00
method: 'POST',
body: JSON.stringify({
submitter_slug: this.submitterSlug,
2024-06-09 23:31:37 +03:00
field_uuid: this.field.uuid,
2024-07-12 15:25:48 +03:00
check_status: checkStatus,
2026-07-28 19:27:25 +03:00
referer: this.normalizedPaymentUrl()
2023-12-14 21:22:26 +02:00
}),
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>