process payment fields
This commit is contained in:
@@ -301,6 +301,7 @@ export default {
|
||||
baseFetch: this.baseFetch,
|
||||
backgroundColor: this.backgroundColor,
|
||||
withPhone: this.withPhone,
|
||||
withPayment: this.withPayment,
|
||||
selectedAreaRef: computed(() => this.selectedAreaRef)
|
||||
}
|
||||
},
|
||||
@@ -369,6 +370,11 @@ export default {
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
withPayment: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
},
|
||||
fetchOptions: {
|
||||
type: Object,
|
||||
required: false,
|
||||
@@ -436,6 +442,12 @@ export default {
|
||||
window.addEventListener('keydown', this.onKeyDown)
|
||||
|
||||
window.addEventListener('resize', this.onWindowResize)
|
||||
|
||||
this.$nextTick(() => {
|
||||
if (document.location.search?.includes('stripe_connect_success')) {
|
||||
document.querySelector('form[action="/auth/stripe_connect"]')?.closest('.dropdown')?.querySelector('label')?.focus()
|
||||
}
|
||||
})
|
||||
},
|
||||
unmounted () {
|
||||
document.removeEventListener('keyup', this.onKeyUp)
|
||||
|
||||
@@ -65,8 +65,12 @@
|
||||
:stroke-width="1.6"
|
||||
/>
|
||||
</button>
|
||||
<PaymentSettings
|
||||
v-if="field.type === 'payment'"
|
||||
:field="field"
|
||||
/>
|
||||
<span
|
||||
v-if="!defaultField"
|
||||
v-else-if="!defaultField"
|
||||
class="dropdown dropdown-end"
|
||||
>
|
||||
<label
|
||||
@@ -261,6 +265,7 @@
|
||||
<script>
|
||||
import Contenteditable from './contenteditable'
|
||||
import FieldType from './field_type'
|
||||
import PaymentSettings from './payment_settings'
|
||||
import { IconShape, IconNewSection, IconTrashX, IconCopy, IconSettings } from '@tabler/icons-vue'
|
||||
import { v4 } from 'uuid'
|
||||
|
||||
@@ -270,6 +275,7 @@ export default {
|
||||
Contenteditable,
|
||||
IconSettings,
|
||||
IconShape,
|
||||
PaymentSettings,
|
||||
IconNewSection,
|
||||
IconTrashX,
|
||||
IconCopy,
|
||||
@@ -295,17 +301,29 @@ export default {
|
||||
emits: ['set-draw', 'remove', 'scroll-to'],
|
||||
data () {
|
||||
return {
|
||||
isNameFocus: false
|
||||
isNameFocus: false,
|
||||
showPaymentModal: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
fieldNames: FieldType.computed.fieldNames,
|
||||
defaultName () {
|
||||
const typeIndex = this.template.fields.filter((f) => f.type === this.field.type).indexOf(this.field)
|
||||
if (this.field.type === 'payment' && this.field.preferences?.price) {
|
||||
const { price, currency } = this.field.preferences || {}
|
||||
|
||||
const suffix = { multiple: 'Select', radio: 'Group' }[this.field.type] || 'Field'
|
||||
const formattedPrice = new Intl.NumberFormat([], {
|
||||
style: 'currency',
|
||||
currency
|
||||
}).format(price)
|
||||
|
||||
return `${this.fieldNames[this.field.type]} ${suffix} ${typeIndex + 1}`
|
||||
return `${this.fieldNames[this.field.type]} ${formattedPrice}`
|
||||
} else {
|
||||
const typeIndex = this.template.fields.filter((f) => f.type === this.field.type).indexOf(this.field)
|
||||
|
||||
const suffix = { multiple: 'Select', radio: 'Group' }[this.field.type] || 'Field'
|
||||
|
||||
return `${this.fieldNames[this.field.type]} ${suffix} ${typeIndex + 1}`
|
||||
}
|
||||
},
|
||||
areas () {
|
||||
return this.field.areas || []
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
v-for="(icon, type) in fieldIcons"
|
||||
:key="type"
|
||||
>
|
||||
<li v-if="withPhone || type !== 'phone'">
|
||||
<li v-if="withPhone || withPayment || !['phone', 'payment'].includes(type)">
|
||||
<a
|
||||
href="#"
|
||||
class="text-sm py-1 px-2"
|
||||
@@ -46,11 +46,11 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconColumns3, IconPhoneCheck, IconLetterCaseUpper } from '@tabler/icons-vue'
|
||||
import { IconTextSize, IconWritingSign, IconCalendarEvent, IconPhoto, IconCheckbox, IconPaperclip, IconSelect, IconCircleDot, IconChecks, IconColumns3, IconPhoneCheck, IconLetterCaseUpper, IconCreditCard } from '@tabler/icons-vue'
|
||||
|
||||
export default {
|
||||
name: 'FiledTypeDropdown',
|
||||
inject: ['withPhone'],
|
||||
inject: ['withPhone', 'withPayment'],
|
||||
props: {
|
||||
modelValue: {
|
||||
type: String,
|
||||
@@ -92,7 +92,8 @@ export default {
|
||||
multiple: 'Multiple',
|
||||
radio: 'Radio',
|
||||
cells: 'Cells',
|
||||
phone: 'Phone'
|
||||
phone: 'Phone',
|
||||
payment: 'Payment'
|
||||
}
|
||||
},
|
||||
fieldIcons () {
|
||||
@@ -108,7 +109,8 @@ export default {
|
||||
radio: IconCircleDot,
|
||||
multiple: IconChecks,
|
||||
cells: IconColumns3,
|
||||
phone: IconPhoneCheck
|
||||
phone: IconPhoneCheck,
|
||||
payment: IconCreditCard
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
:key="type"
|
||||
>
|
||||
<button
|
||||
v-if="withPhone || type != 'phone'"
|
||||
v-if="(withPhone || type != 'phone') && (withPayment || type != 'payment')"
|
||||
draggable="true"
|
||||
class="group flex items-center justify-center border border-dashed border-base-300 hover:border-base-content/20 w-full rounded relative"
|
||||
:style="{ backgroundColor: backgroundColor }"
|
||||
@@ -90,7 +90,7 @@
|
||||
</div>
|
||||
</button>
|
||||
<div
|
||||
v-else
|
||||
v-else-if="type == 'phone'"
|
||||
class="tooltip tooltip-bottom-end flex"
|
||||
data-tip="Unlock SMS-verified phone number field with paid plan. Use text field for phone numbers without verification."
|
||||
>
|
||||
@@ -152,7 +152,7 @@ export default {
|
||||
IconDrag,
|
||||
IconLock
|
||||
},
|
||||
inject: ['save', 'backgroundColor', 'withPhone'],
|
||||
inject: ['save', 'backgroundColor', 'withPhone', 'withPayment'],
|
||||
props: {
|
||||
fields: {
|
||||
type: Array,
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
<template>
|
||||
<span
|
||||
class="dropdown dropdown-end"
|
||||
:class="{ 'dropdown-open': (!field.preferences?.price || !isConnected) && !isLoading }"
|
||||
>
|
||||
<label
|
||||
tabindex="0"
|
||||
title="Settings"
|
||||
class="cursor-pointer text-transparent group-hover:text-base-content"
|
||||
>
|
||||
<IconSettings
|
||||
:width="18"
|
||||
:stroke-width="1.6"
|
||||
/>
|
||||
</label>
|
||||
<ul
|
||||
tabindex="0"
|
||||
class="mt-1.5 dropdown-content menu menu-xs p-2 shadow bg-base-100 rounded-box w-52 z-10"
|
||||
draggable="true"
|
||||
@dragstart.prevent.stop
|
||||
@click="closeDropdown"
|
||||
>
|
||||
<div
|
||||
class="py-1.5 px-1 relative"
|
||||
@click.stop
|
||||
>
|
||||
<select
|
||||
v-model="field.preferences.currency"
|
||||
placeholder="Price"
|
||||
class="select select-bordered select-xs font-normal w-full max-w-xs !h-7 !outline-0"
|
||||
@change="save"
|
||||
>
|
||||
<option
|
||||
v-for="currency in currencies"
|
||||
:key="currency"
|
||||
:value="currency"
|
||||
>
|
||||
{{ currency }}
|
||||
</option>
|
||||
</select>
|
||||
<label
|
||||
:style="{ backgroundColor: backgroundColor }"
|
||||
class="absolute -top-1 left-2.5 px-1 h-4"
|
||||
style="font-size: 8px"
|
||||
>
|
||||
Currency
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
class="py-1.5 px-1 relative"
|
||||
@click.stop
|
||||
>
|
||||
<input
|
||||
v-model="field.preferences.price"
|
||||
type="number"
|
||||
placeholder="Price"
|
||||
class="input input-bordered input-xs w-full max-w-xs h-7 !outline-0"
|
||||
@blur="save"
|
||||
>
|
||||
<label
|
||||
v-if="field.preferences.price"
|
||||
:style="{ backgroundColor: backgroundColor }"
|
||||
class="absolute -top-1 left-2.5 px-1 h-4"
|
||||
style="font-size: 8px"
|
||||
>
|
||||
Price
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
v-if="!isConnected || isOauthSuccess"
|
||||
class="py-1.5 px-1 relative"
|
||||
@click.stop
|
||||
>
|
||||
<div
|
||||
v-if="isConnected && isOauthSuccess"
|
||||
class="text-sm text-center"
|
||||
>
|
||||
<IconCircleCheck
|
||||
class="inline text-green-600 w-4 h-4"
|
||||
/>
|
||||
Stripe Connected
|
||||
</div>
|
||||
<form
|
||||
v-if="!isConnected"
|
||||
data-turbo="false"
|
||||
action="/auth/stripe_connect"
|
||||
accept-charset="UTF-8"
|
||||
target="_blank"
|
||||
method="post"
|
||||
>
|
||||
<input
|
||||
type="hidden"
|
||||
name="state"
|
||||
:value="oauthState"
|
||||
autocomplete="off"
|
||||
>
|
||||
<input
|
||||
type="hidden"
|
||||
name="redirect_uri"
|
||||
:value="redirectUri"
|
||||
autocomplete="off"
|
||||
>
|
||||
<input
|
||||
type="hidden"
|
||||
name="scope"
|
||||
value="read_write"
|
||||
autocomplete="off"
|
||||
>
|
||||
<input
|
||||
type="hidden"
|
||||
name="authenticity_token"
|
||||
:value="authenticityToken"
|
||||
autocomplete="off"
|
||||
>
|
||||
<button
|
||||
type="submit"
|
||||
:disabled="isLoading"
|
||||
class="btn bg-[#7B73FF] hover:bg-[#0A2540] btn-sm text-white w-full"
|
||||
>
|
||||
<span
|
||||
v-if="isLoading"
|
||||
class="flex items-center space-x-1"
|
||||
>
|
||||
<IconInnerShadowTop
|
||||
class="w-4 h-4 animate-spin inline"
|
||||
/>
|
||||
<span>
|
||||
Connect Stripe
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
v-else
|
||||
class="flex items-center space-x-1"
|
||||
>
|
||||
<IconBrandStripe
|
||||
class="w-4 h-4 inline"
|
||||
/>
|
||||
<span>
|
||||
Connect Stripe
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</ul>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { IconSettings, IconCircleCheck, IconBrandStripe, IconInnerShadowTop } from '@tabler/icons-vue'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const isConnected = ref(false)
|
||||
|
||||
export default {
|
||||
name: 'PaymentSettings',
|
||||
components: {
|
||||
IconSettings,
|
||||
IconCircleCheck,
|
||||
IconInnerShadowTop,
|
||||
IconBrandStripe
|
||||
},
|
||||
inject: ['backgroundColor', 'save'],
|
||||
props: {
|
||||
field: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
isLoading: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isConnected: () => isConnected.value,
|
||||
isOauthSuccess () {
|
||||
return document.location.search?.includes('stripe_connect_success')
|
||||
},
|
||||
redirectUri () {
|
||||
return document.location.origin + '/auth/stripe_connect/callback'
|
||||
},
|
||||
currencies () {
|
||||
return ['USD', 'EUR', 'GBP']
|
||||
},
|
||||
authenticityToken () {
|
||||
return document.querySelector('meta[name="csrf-token"]')?.content
|
||||
},
|
||||
oauthState () {
|
||||
const params = new URLSearchParams('')
|
||||
|
||||
params.set('redir', document.location.href)
|
||||
|
||||
return params.toString()
|
||||
},
|
||||
defaultCurrency () {
|
||||
const userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||
|
||||
if (userTimezone.startsWith('Europe')) {
|
||||
return 'EUR'
|
||||
} else if (userTimezone.includes('London') || userTimezone.includes('Belfast')) {
|
||||
return 'GBP'
|
||||
} else {
|
||||
return 'USD'
|
||||
}
|
||||
}
|
||||
},
|
||||
created () {
|
||||
this.field.preferences ||= {}
|
||||
},
|
||||
mounted () {
|
||||
this.field.preferences.currency ||= this.defaultCurrency
|
||||
|
||||
if (!this.isConnected) {
|
||||
this.checkStatus()
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
checkStatus () {
|
||||
this.isLoading = true
|
||||
|
||||
fetch('/api/stripe_connect').then(async (resp) => {
|
||||
const { status } = await resp.json()
|
||||
|
||||
if (status === 'connected') {
|
||||
isConnected.value = true
|
||||
}
|
||||
}).finally(() => {
|
||||
this.isLoading = false
|
||||
})
|
||||
},
|
||||
closeDropdown () {
|
||||
document.activeElement.blur()
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user