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
@@ -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)
+23 -5
View File
@@ -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
}
}
},
+3 -3
View File
@@ -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>