process payment fields
This commit is contained in:
@@ -21,10 +21,6 @@ gem 'jwt'
|
|||||||
gem 'lograge'
|
gem 'lograge'
|
||||||
gem 'mysql2', require: false
|
gem 'mysql2', require: false
|
||||||
gem 'oj'
|
gem 'oj'
|
||||||
gem 'omniauth-github'
|
|
||||||
gem 'omniauth-google-oauth2'
|
|
||||||
gem 'omniauth-microsoft-office365'
|
|
||||||
gem 'omniauth-rails_csrf_protection'
|
|
||||||
gem 'pagy'
|
gem 'pagy'
|
||||||
gem 'pdf-reader'
|
gem 'pdf-reader'
|
||||||
gem 'pg', require: false
|
gem 'pg', require: false
|
||||||
|
|||||||
@@ -245,7 +245,6 @@ GEM
|
|||||||
signet (>= 0.16, < 2.a)
|
signet (>= 0.16, < 2.a)
|
||||||
hashdiff (1.0.1)
|
hashdiff (1.0.1)
|
||||||
hashery (2.1.2)
|
hashery (2.1.2)
|
||||||
hashie (5.0.0)
|
|
||||||
hexapdf (0.34.1)
|
hexapdf (0.34.1)
|
||||||
cmdparse (~> 3.0, >= 3.0.3)
|
cmdparse (~> 3.0, >= 3.0.3)
|
||||||
geom2d (~> 0.4, >= 0.4.1)
|
geom2d (~> 0.4, >= 0.4.1)
|
||||||
@@ -296,7 +295,6 @@ GEM
|
|||||||
minitest (5.20.0)
|
minitest (5.20.0)
|
||||||
msgpack (1.7.2)
|
msgpack (1.7.2)
|
||||||
multi_json (1.15.0)
|
multi_json (1.15.0)
|
||||||
multi_xml (0.6.0)
|
|
||||||
multipart-post (2.3.0)
|
multipart-post (2.3.0)
|
||||||
mysql2 (0.5.5)
|
mysql2 (0.5.5)
|
||||||
net-http-persistent (4.0.2)
|
net-http-persistent (4.0.2)
|
||||||
@@ -316,35 +314,7 @@ GEM
|
|||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
nokogiri (1.15.4-arm64-darwin)
|
nokogiri (1.15.4-arm64-darwin)
|
||||||
racc (~> 1.4)
|
racc (~> 1.4)
|
||||||
oauth2 (2.0.9)
|
|
||||||
faraday (>= 0.17.3, < 3.0)
|
|
||||||
jwt (>= 1.0, < 3.0)
|
|
||||||
multi_xml (~> 0.5)
|
|
||||||
rack (>= 1.2, < 4)
|
|
||||||
snaky_hash (~> 2.0)
|
|
||||||
version_gem (~> 1.1)
|
|
||||||
oj (3.16.0)
|
oj (3.16.0)
|
||||||
omniauth (2.1.1)
|
|
||||||
hashie (>= 3.4.6)
|
|
||||||
rack (>= 2.2.3)
|
|
||||||
rack-protection
|
|
||||||
omniauth-github (2.0.1)
|
|
||||||
omniauth (~> 2.0)
|
|
||||||
omniauth-oauth2 (~> 1.8)
|
|
||||||
omniauth-google-oauth2 (1.1.1)
|
|
||||||
jwt (>= 2.0)
|
|
||||||
oauth2 (~> 2.0.6)
|
|
||||||
omniauth (~> 2.0)
|
|
||||||
omniauth-oauth2 (~> 1.8.0)
|
|
||||||
omniauth-microsoft-office365 (0.0.8)
|
|
||||||
omniauth
|
|
||||||
omniauth-oauth2
|
|
||||||
omniauth-oauth2 (1.8.0)
|
|
||||||
oauth2 (>= 1.4, < 3)
|
|
||||||
omniauth (~> 2.0)
|
|
||||||
omniauth-rails_csrf_protection (1.0.1)
|
|
||||||
actionpack (>= 4.2)
|
|
||||||
omniauth (~> 2.0)
|
|
||||||
openssl (3.2.0)
|
openssl (3.2.0)
|
||||||
orm_adapter (0.5.0)
|
orm_adapter (0.5.0)
|
||||||
os (1.1.4)
|
os (1.1.4)
|
||||||
@@ -378,8 +348,6 @@ GEM
|
|||||||
nio4r (~> 2.0)
|
nio4r (~> 2.0)
|
||||||
racc (1.7.1)
|
racc (1.7.1)
|
||||||
rack (2.2.8)
|
rack (2.2.8)
|
||||||
rack-protection (3.1.0)
|
|
||||||
rack (~> 2.2, >= 2.2.4)
|
|
||||||
rack-proxy (0.7.6)
|
rack-proxy (0.7.6)
|
||||||
rack
|
rack
|
||||||
rack-test (2.1.0)
|
rack-test (2.1.0)
|
||||||
@@ -520,9 +488,6 @@ GEM
|
|||||||
simplecov-html (0.12.3)
|
simplecov-html (0.12.3)
|
||||||
simplecov_json_formatter (0.1.4)
|
simplecov_json_formatter (0.1.4)
|
||||||
smart_properties (1.17.0)
|
smart_properties (1.17.0)
|
||||||
snaky_hash (2.0.1)
|
|
||||||
hashie
|
|
||||||
version_gem (~> 1.1, >= 1.1.1)
|
|
||||||
sqlite3 (1.6.3)
|
sqlite3 (1.6.3)
|
||||||
mini_portile2 (~> 2.8.0)
|
mini_portile2 (~> 2.8.0)
|
||||||
sqlite3 (1.6.3-arm64-darwin)
|
sqlite3 (1.6.3-arm64-darwin)
|
||||||
@@ -543,7 +508,6 @@ GEM
|
|||||||
uber (0.1.0)
|
uber (0.1.0)
|
||||||
unicode-display_width (2.5.0)
|
unicode-display_width (2.5.0)
|
||||||
uniform_notifier (1.16.0)
|
uniform_notifier (1.16.0)
|
||||||
version_gem (1.1.3)
|
|
||||||
warden (1.2.9)
|
warden (1.2.9)
|
||||||
rack (>= 2.0.9)
|
rack (>= 2.0.9)
|
||||||
web-console (4.2.0)
|
web-console (4.2.0)
|
||||||
@@ -595,10 +559,6 @@ DEPENDENCIES
|
|||||||
lograge
|
lograge
|
||||||
mysql2
|
mysql2
|
||||||
oj
|
oj
|
||||||
omniauth-github
|
|
||||||
omniauth-google-oauth2
|
|
||||||
omniauth-microsoft-office365
|
|
||||||
omniauth-rails_csrf_protection
|
|
||||||
pagy
|
pagy
|
||||||
pdf-reader
|
pdf-reader
|
||||||
pg
|
pg
|
||||||
|
|||||||
@@ -58,7 +58,9 @@ module Api
|
|||||||
schema: [%i[attachment_uuid name]],
|
schema: [%i[attachment_uuid name]],
|
||||||
submitters: [%i[name uuid]],
|
submitters: [%i[name uuid]],
|
||||||
fields: [[:uuid, :submitter_uuid, :name, :type, :required, :readonly, :default_value,
|
fields: [[:uuid, :submitter_uuid, :name, :type, :required, :readonly, :default_value,
|
||||||
{ options: [%i[value uuid]], areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]]
|
{ preferences: {},
|
||||||
|
options: [%i[value uuid]],
|
||||||
|
areas: [%i[x y w h cell_w attachment_uuid option_uuid page]] }]]
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
class OmniauthCallbacksController < Devise::OmniauthCallbacksController
|
|
||||||
def google_oauth2
|
|
||||||
@user = Users.from_omniauth(request.env['omniauth.auth'])
|
|
||||||
|
|
||||||
if @user.persisted?
|
|
||||||
flash[:notice] = I18n.t('devise.omniauth_callbacks.success', kind: 'Google')
|
|
||||||
|
|
||||||
sign_in_and_redirect @user, event: :authentication
|
|
||||||
else
|
|
||||||
redirect_to new_registration_path(oauth_callback: true, user: @user.slice(:email, :first_name, :last_name)),
|
|
||||||
notice: 'Please complete registration with Google auth'
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
@@ -86,6 +86,7 @@ window.customElements.define('template-builder', class extends HTMLElement {
|
|||||||
backgroundColor: '#faf7f5',
|
backgroundColor: '#faf7f5',
|
||||||
withPhone: this.dataset.withPhone === 'true',
|
withPhone: this.dataset.withPhone === 'true',
|
||||||
withLogo: this.dataset.withLogo !== 'false',
|
withLogo: this.dataset.withLogo !== 'false',
|
||||||
|
withPayment: this.dataset.withPayment !== 'false',
|
||||||
acceptFileTypes: this.dataset.acceptFileTypes,
|
acceptFileTypes: this.dataset.acceptFileTypes,
|
||||||
isDirectUpload: this.dataset.isDirectUpload === 'true'
|
isDirectUpload: this.dataset.isDirectUpload === 'true'
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ window.customElements.define('submission-form', class extends HTMLElement {
|
|||||||
|
|
||||||
this.app = createApp(Form, {
|
this.app = createApp(Form, {
|
||||||
submitter: JSON.parse(this.dataset.submitter),
|
submitter: JSON.parse(this.dataset.submitter),
|
||||||
authenticityToken: this.dataset.authenticityToken,
|
|
||||||
canSendEmail: this.dataset.canSendEmail === 'true',
|
canSendEmail: this.dataset.canSendEmail === 'true',
|
||||||
isDirectUpload: this.dataset.isDirectUpload === 'true',
|
isDirectUpload: this.dataset.isDirectUpload === 'true',
|
||||||
goToLast: this.dataset.goToLast === 'true',
|
goToLast: this.dataset.goToLast === 'true',
|
||||||
|
|||||||
@@ -53,7 +53,7 @@
|
|||||||
:src="initials.url"
|
:src="initials.url"
|
||||||
>
|
>
|
||||||
<div
|
<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"
|
class="px-0.5 flex flex-col justify-center"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
@@ -158,7 +158,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<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 {
|
export default {
|
||||||
name: 'FieldArea',
|
name: 'FieldArea',
|
||||||
@@ -231,7 +231,8 @@ export default {
|
|||||||
checkbox: 'Checkbox',
|
checkbox: 'Checkbox',
|
||||||
radio: 'Radio',
|
radio: 'Radio',
|
||||||
multiple: 'Multiple Select',
|
multiple: 'Multiple Select',
|
||||||
phone: 'Phone'
|
phone: 'Phone',
|
||||||
|
payment: 'Payment'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
fieldIcons () {
|
fieldIcons () {
|
||||||
@@ -247,7 +248,8 @@ export default {
|
|||||||
radio: IconCircleDot,
|
radio: IconCircleDot,
|
||||||
cells: IconColumns3,
|
cells: IconColumns3,
|
||||||
multiple: IconChecks,
|
multiple: IconChecks,
|
||||||
phone: IconPhoneCheck
|
phone: IconPhoneCheck,
|
||||||
|
payment: IconCreditCard
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
image () {
|
image () {
|
||||||
@@ -281,6 +283,8 @@ export default {
|
|||||||
attachments () {
|
attachments () {
|
||||||
if (this.field.type === 'file') {
|
if (this.field.type === 'file') {
|
||||||
return (this.modelValue || []).map((uuid) => this.attachmentsIndex[uuid])
|
return (this.modelValue || []).map((uuid) => this.attachmentsIndex[uuid])
|
||||||
|
} else if (this.field.type === 'payment') {
|
||||||
|
return [this.attachmentsIndex[this.modelValue]].filter(Boolean)
|
||||||
} else {
|
} else {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -266,8 +266,22 @@
|
|||||||
@focus="$refs.areas.scrollIntoField(currentField)"
|
@focus="$refs.areas.scrollIntoField(currentField)"
|
||||||
@submit="submitStep"
|
@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>
|
||||||
<div class="mt-6 md:mt-8">
|
<div
|
||||||
|
v-if="currentField.type !== 'payment' || submittedValues[currentField.uuid]"
|
||||||
|
class="mt-6 md:mt-8"
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
ref="submitButton"
|
ref="submitButton"
|
||||||
type="submit"
|
type="submit"
|
||||||
@@ -331,6 +345,7 @@ import InitialsStep from './initials_step'
|
|||||||
import AttachmentStep from './attachment_step'
|
import AttachmentStep from './attachment_step'
|
||||||
import MultiSelectStep from './multi_select_step'
|
import MultiSelectStep from './multi_select_step'
|
||||||
import PhoneStep from './phone_step'
|
import PhoneStep from './phone_step'
|
||||||
|
import PaymentStep from './payment_step'
|
||||||
import TextStep from './text_step'
|
import TextStep from './text_step'
|
||||||
import DateStep from './date_step'
|
import DateStep from './date_step'
|
||||||
import FormCompleted from './completed'
|
import FormCompleted from './completed'
|
||||||
@@ -351,6 +366,7 @@ export default {
|
|||||||
IconArrowsDiagonal,
|
IconArrowsDiagonal,
|
||||||
TextStep,
|
TextStep,
|
||||||
PhoneStep,
|
PhoneStep,
|
||||||
|
PaymentStep,
|
||||||
IconArrowsDiagonalMinimize2,
|
IconArrowsDiagonalMinimize2,
|
||||||
FormCompleted
|
FormCompleted
|
||||||
},
|
},
|
||||||
@@ -402,11 +418,6 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: () => []
|
default: () => []
|
||||||
},
|
},
|
||||||
authenticityToken: {
|
|
||||||
type: String,
|
|
||||||
required: false,
|
|
||||||
default: ''
|
|
||||||
},
|
|
||||||
backgroundColor: {
|
backgroundColor: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -463,6 +474,12 @@ export default {
|
|||||||
currentStepFields () {
|
currentStepFields () {
|
||||||
return this.stepFields[this.currentStep]
|
return this.stepFields[this.currentStep]
|
||||||
},
|
},
|
||||||
|
queryParams () {
|
||||||
|
return new URLSearchParams(window.location.search)
|
||||||
|
},
|
||||||
|
authenticityToken () {
|
||||||
|
return document.querySelector('meta[name="csrf-token"]')?.content
|
||||||
|
},
|
||||||
submitterSlug () {
|
submitterSlug () {
|
||||||
return this.submitter.slug
|
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 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
|
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: {
|
methods: {
|
||||||
t,
|
t,
|
||||||
maybeTrackEmailClick () {
|
maybeTrackEmailClick () {
|
||||||
const queryParams = new URLSearchParams(window.location.search)
|
const { queryParams } = this
|
||||||
|
|
||||||
if (queryParams.has('t')) {
|
if (queryParams.has('t')) {
|
||||||
const t = queryParams.get('t')
|
const t = queryParams.get('t')
|
||||||
@@ -594,7 +617,7 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
maybeTrackSmsClick () {
|
maybeTrackSmsClick () {
|
||||||
const queryParams = new URLSearchParams(window.location.search)
|
const { queryParams } = this
|
||||||
|
|
||||||
if (queryParams.has('c')) {
|
if (queryParams.has('c')) {
|
||||||
const c = queryParams.get('c')
|
const c = queryParams.get('c')
|
||||||
@@ -667,7 +690,7 @@ export default {
|
|||||||
async submitStep () {
|
async submitStep () {
|
||||||
this.isSubmitting = true
|
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
|
? this.$refs.currentStep.submit
|
||||||
: () => Promise.resolve({})
|
: () => Promise.resolve({})
|
||||||
|
|
||||||
@@ -684,7 +707,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await this.saveStep(formData).then(async (response) => {
|
await this.saveStep(formData).then(async (response) => {
|
||||||
if (response.status === 422) {
|
if (response.status === 422 || response.status === 500) {
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
|
|
||||||
alert(data.error || 'Value is invalid')
|
alert(data.error || 'Value is invalid')
|
||||||
@@ -714,7 +737,7 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}).catch(error => {
|
}).catch(error => {
|
||||||
console.error('Error submitting form:', error)
|
alert(error)
|
||||||
}).finally(() => {
|
}).finally(() => {
|
||||||
this.isSubmitting = false
|
this.isSubmitting = false
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -38,7 +38,9 @@ const en = {
|
|||||||
type: 'Type',
|
type: 'Type',
|
||||||
type_text: 'Type text',
|
type_text: 'Type text',
|
||||||
date: 'Date',
|
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 = {
|
const es = {
|
||||||
@@ -81,7 +83,9 @@ const es = {
|
|||||||
draw: 'Dibujar',
|
draw: 'Dibujar',
|
||||||
type: 'Escribir',
|
type: 'Escribir',
|
||||||
type_text: 'Escribir texto',
|
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 = {
|
const it = {
|
||||||
@@ -124,7 +128,9 @@ const it = {
|
|||||||
type: 'Inserisci',
|
type: 'Inserisci',
|
||||||
type_text: 'Inserisci testo',
|
type_text: 'Inserisci testo',
|
||||||
toggle_multiline_text: 'Attiva Testo Multilinea',
|
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 = {
|
const de = {
|
||||||
@@ -167,7 +173,9 @@ const de = {
|
|||||||
type: 'Eingeben',
|
type: 'Eingeben',
|
||||||
type_text: 'Text eingeben',
|
type_text: 'Text eingeben',
|
||||||
toggle_multiline_text: 'Mehrzeiligen Text umschalten',
|
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 = {
|
const fr = {
|
||||||
@@ -210,7 +218,9 @@ const fr = {
|
|||||||
type: 'Saisir',
|
type: 'Saisir',
|
||||||
type_text: 'Saisir du texte',
|
type_text: 'Saisir du texte',
|
||||||
toggle_multiline_text: 'Basculer le Texte Multiligne',
|
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 = {
|
const pl = {
|
||||||
@@ -253,7 +263,9 @@ const pl = {
|
|||||||
type: 'Wprowadź',
|
type: 'Wprowadź',
|
||||||
type_text: 'Wprowadź tekst',
|
type_text: 'Wprowadź tekst',
|
||||||
toggle_multiline_text: 'Przełącz Tekst Wielolinijkowy',
|
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 = {
|
const uk = {
|
||||||
@@ -296,7 +308,9 @@ const uk = {
|
|||||||
type: 'Текст',
|
type: 'Текст',
|
||||||
type_text: 'Введіть текст',
|
type_text: 'Введіть текст',
|
||||||
toggle_multiline_text: 'Перемкнути Багаторядковий Текст',
|
toggle_multiline_text: 'Перемкнути Багаторядковий Текст',
|
||||||
email_has_been_sent: 'Електронний лист був відправлений'
|
email_has_been_sent: 'Електронний лист був відправлений',
|
||||||
|
processing: 'Обробка',
|
||||||
|
pay_with_strip: 'Сплатити за допомогою Stripe'
|
||||||
}
|
}
|
||||||
|
|
||||||
const cs = {
|
const cs = {
|
||||||
@@ -339,7 +353,9 @@ const cs = {
|
|||||||
type: 'Zadat',
|
type: 'Zadat',
|
||||||
type_text: 'Zadat text',
|
type_text: 'Zadat text',
|
||||||
toggle_multiline_text: 'Přepnout Víceřádkový 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 = {
|
const pt = {
|
||||||
@@ -382,7 +398,9 @@ const pt = {
|
|||||||
type: 'Inserir',
|
type: 'Inserir',
|
||||||
type_text: 'Inserir texto',
|
type_text: 'Inserir texto',
|
||||||
toggle_multiline_text: 'Alternar Texto Multilinha',
|
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 }
|
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>
|
||||||
@@ -301,6 +301,7 @@ export default {
|
|||||||
baseFetch: this.baseFetch,
|
baseFetch: this.baseFetch,
|
||||||
backgroundColor: this.backgroundColor,
|
backgroundColor: this.backgroundColor,
|
||||||
withPhone: this.withPhone,
|
withPhone: this.withPhone,
|
||||||
|
withPayment: this.withPayment,
|
||||||
selectedAreaRef: computed(() => this.selectedAreaRef)
|
selectedAreaRef: computed(() => this.selectedAreaRef)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -369,6 +370,11 @@ export default {
|
|||||||
required: false,
|
required: false,
|
||||||
default: false
|
default: false
|
||||||
},
|
},
|
||||||
|
withPayment: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
fetchOptions: {
|
fetchOptions: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: false,
|
required: false,
|
||||||
@@ -436,6 +442,12 @@ export default {
|
|||||||
window.addEventListener('keydown', this.onKeyDown)
|
window.addEventListener('keydown', this.onKeyDown)
|
||||||
|
|
||||||
window.addEventListener('resize', this.onWindowResize)
|
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 () {
|
unmounted () {
|
||||||
document.removeEventListener('keyup', this.onKeyUp)
|
document.removeEventListener('keyup', this.onKeyUp)
|
||||||
|
|||||||
@@ -65,8 +65,12 @@
|
|||||||
:stroke-width="1.6"
|
:stroke-width="1.6"
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
|
<PaymentSettings
|
||||||
|
v-if="field.type === 'payment'"
|
||||||
|
:field="field"
|
||||||
|
/>
|
||||||
<span
|
<span
|
||||||
v-if="!defaultField"
|
v-else-if="!defaultField"
|
||||||
class="dropdown dropdown-end"
|
class="dropdown dropdown-end"
|
||||||
>
|
>
|
||||||
<label
|
<label
|
||||||
@@ -261,6 +265,7 @@
|
|||||||
<script>
|
<script>
|
||||||
import Contenteditable from './contenteditable'
|
import Contenteditable from './contenteditable'
|
||||||
import FieldType from './field_type'
|
import FieldType from './field_type'
|
||||||
|
import PaymentSettings from './payment_settings'
|
||||||
import { IconShape, IconNewSection, IconTrashX, IconCopy, IconSettings } from '@tabler/icons-vue'
|
import { IconShape, IconNewSection, IconTrashX, IconCopy, IconSettings } from '@tabler/icons-vue'
|
||||||
import { v4 } from 'uuid'
|
import { v4 } from 'uuid'
|
||||||
|
|
||||||
@@ -270,6 +275,7 @@ export default {
|
|||||||
Contenteditable,
|
Contenteditable,
|
||||||
IconSettings,
|
IconSettings,
|
||||||
IconShape,
|
IconShape,
|
||||||
|
PaymentSettings,
|
||||||
IconNewSection,
|
IconNewSection,
|
||||||
IconTrashX,
|
IconTrashX,
|
||||||
IconCopy,
|
IconCopy,
|
||||||
@@ -295,17 +301,29 @@ export default {
|
|||||||
emits: ['set-draw', 'remove', 'scroll-to'],
|
emits: ['set-draw', 'remove', 'scroll-to'],
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
isNameFocus: false
|
isNameFocus: false,
|
||||||
|
showPaymentModal: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
fieldNames: FieldType.computed.fieldNames,
|
fieldNames: FieldType.computed.fieldNames,
|
||||||
defaultName () {
|
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 () {
|
areas () {
|
||||||
return this.field.areas || []
|
return this.field.areas || []
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
v-for="(icon, type) in fieldIcons"
|
v-for="(icon, type) in fieldIcons"
|
||||||
:key="type"
|
:key="type"
|
||||||
>
|
>
|
||||||
<li v-if="withPhone || type !== 'phone'">
|
<li v-if="withPhone || withPayment || !['phone', 'payment'].includes(type)">
|
||||||
<a
|
<a
|
||||||
href="#"
|
href="#"
|
||||||
class="text-sm py-1 px-2"
|
class="text-sm py-1 px-2"
|
||||||
@@ -46,11 +46,11 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<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 {
|
export default {
|
||||||
name: 'FiledTypeDropdown',
|
name: 'FiledTypeDropdown',
|
||||||
inject: ['withPhone'],
|
inject: ['withPhone', 'withPayment'],
|
||||||
props: {
|
props: {
|
||||||
modelValue: {
|
modelValue: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -92,7 +92,8 @@ export default {
|
|||||||
multiple: 'Multiple',
|
multiple: 'Multiple',
|
||||||
radio: 'Radio',
|
radio: 'Radio',
|
||||||
cells: 'Cells',
|
cells: 'Cells',
|
||||||
phone: 'Phone'
|
phone: 'Phone',
|
||||||
|
payment: 'Payment'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
fieldIcons () {
|
fieldIcons () {
|
||||||
@@ -108,7 +109,8 @@ export default {
|
|||||||
radio: IconCircleDot,
|
radio: IconCircleDot,
|
||||||
multiple: IconChecks,
|
multiple: IconChecks,
|
||||||
cells: IconColumns3,
|
cells: IconColumns3,
|
||||||
phone: IconPhoneCheck
|
phone: IconPhoneCheck,
|
||||||
|
payment: IconCreditCard
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -71,7 +71,7 @@
|
|||||||
:key="type"
|
:key="type"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
v-if="withPhone || type != 'phone'"
|
v-if="(withPhone || type != 'phone') && (withPayment || type != 'payment')"
|
||||||
draggable="true"
|
draggable="true"
|
||||||
class="group flex items-center justify-center border border-dashed border-base-300 hover:border-base-content/20 w-full rounded relative"
|
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 }"
|
:style="{ backgroundColor: backgroundColor }"
|
||||||
@@ -90,7 +90,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
<div
|
<div
|
||||||
v-else
|
v-else-if="type == 'phone'"
|
||||||
class="tooltip tooltip-bottom-end flex"
|
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."
|
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,
|
IconDrag,
|
||||||
IconLock
|
IconLock
|
||||||
},
|
},
|
||||||
inject: ['save', 'backgroundColor', 'withPhone'],
|
inject: ['save', 'backgroundColor', 'withPhone', 'withPayment'],
|
||||||
props: {
|
props: {
|
||||||
fields: {
|
fields: {
|
||||||
type: Array,
|
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>
|
||||||
@@ -100,7 +100,7 @@ class SubmitterMailer < ApplicationMailer
|
|||||||
|
|
||||||
attachments[submitter.submission.audit_trail.filename.to_s] = audit_trail_data if audit_trail_data
|
attachments[submitter.submission.audit_trail.filename.to_s] = audit_trail_data if audit_trail_data
|
||||||
|
|
||||||
file_fields = submitter.submission.template_fields.select { |e| e['type'] == 'file' }
|
file_fields = submitter.submission.template_fields.select { |e| e['type'].in?(%w[file payment]) }
|
||||||
|
|
||||||
if file_fields.pluck('submitter_uuid').uniq.size == 1
|
if file_fields.pluck('submitter_uuid').uniq.size == 1
|
||||||
storage_attachments =
|
storage_attachments =
|
||||||
|
|||||||
@@ -59,10 +59,6 @@ class User < ApplicationRecord
|
|||||||
|
|
||||||
devise :two_factor_authenticatable, :recoverable, :rememberable, :validatable, :trackable
|
devise :two_factor_authenticatable, :recoverable, :rememberable, :validatable, :trackable
|
||||||
|
|
||||||
if Docuseal.multitenant?
|
|
||||||
devise :registerable, :omniauthable, omniauth_providers: %i[google_oauth2 microsoft_office365 github]
|
|
||||||
end
|
|
||||||
|
|
||||||
attribute :role, :string, default: ADMIN_ROLE
|
attribute :role, :string, default: ADMIN_ROLE
|
||||||
attribute :uuid, :string, default: -> { SecureRandom.uuid }
|
attribute :uuid, :string, default: -> { SecureRandom.uuid }
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<field-value class="flex absolute text-[1.5vw] lg:text-base" style="width: <%= area['w'] * 100 %>%; height: <%= area['h'] * 100 %>%; left: <%= area['x'] * 100 %>%; top: <%= area['y'] * 100 %>%">
|
<field-value class="flex absolute text-[1.5vw] lg:text-base" style="width: <%= area['w'] * 100 %>%; height: <%= area['h'] * 100 %>%; left: <%= area['x'] * 100 %>%; top: <%= area['y'] * 100 %>%">
|
||||||
<% if field['type'].in?(['signature', 'image', 'initials']) %>
|
<% if field['type'].in?(['signature', 'image', 'initials']) %>
|
||||||
<img class="object-contain mx-auto" src="<%= attachments_index[value].url %>" loading="lazy">
|
<img class="object-contain mx-auto" src="<%= attachments_index[value].url %>" loading="lazy">
|
||||||
<% elsif field['type'] == 'file' %>
|
<% elsif field['type'].in?(['file', 'payment']) %>
|
||||||
<autosize-field></autosize-field>
|
<autosize-field></autosize-field>
|
||||||
<div class="px-0.5 flex flex-col justify-center">
|
<div class="px-0.5 flex flex-col justify-center">
|
||||||
<% Array.wrap(value).each do |val| %>
|
<% Array.wrap(value).each do |val| %>
|
||||||
|
|||||||
@@ -148,7 +148,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<% elsif field['type'] == 'image' %>
|
<% elsif field['type'] == 'image' %>
|
||||||
<img class="object-contain mx-auto max-h-28" height="<%= attachments_index[value].metadata['height'] %>" width="<%= attachments_index[value].metadata['width'] %>" src="<%= attachments_index[value].url %>" loading="lazy">
|
<img class="object-contain mx-auto max-h-28" height="<%= attachments_index[value].metadata['height'] %>" width="<%= attachments_index[value].metadata['width'] %>" src="<%= attachments_index[value].url %>" loading="lazy">
|
||||||
<% elsif field['type'] == 'file' %>
|
<% elsif field['type'] == 'file' || field['type'] == 'payment' %>
|
||||||
<div class="flex flex-col justify-center">
|
<div class="flex flex-col justify-center">
|
||||||
<% Array.wrap(value).each do |val| %>
|
<% Array.wrap(value).each do |val| %>
|
||||||
<a target="_blank" class="flex items-center space-x-1" href="<%= attachments_index[val].url %>">
|
<a target="_blank" class="flex items-center space-x-1" href="<%= attachments_index[val].url %>">
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<% data_attachments = attachments_index.values.select { |e| e.record_id == submitter.id }.to_json(only: %i[uuid], methods: %i[url filename content_type]) %>
|
<% data_attachments = attachments_index.values.select { |e| e.record_id == submitter.id }.to_json(only: %i[uuid], methods: %i[url filename content_type]) %>
|
||||||
<% data_fields = (submitter.submission.template_fields || submitter.submission.template.fields).select { |f| f['submitter_uuid'] == submitter.uuid }.to_json %>
|
<% data_fields = (submitter.submission.template_fields || submitter.submission.template.fields).select { |f| f['submitter_uuid'] == submitter.uuid }.to_json %>
|
||||||
<% configs = Submitters::FormConfigs.call(submitter) %>
|
<% configs = Submitters::FormConfigs.call(submitter) %>
|
||||||
<submission-form data-is-demo="<%= Docuseal.demo? %>" data-completed-button="<%= configs[:completed_button].to_json %>" data-go-to-last="<%= submitter.opened_at? %>" data-is-direct-upload="<%= Docuseal.active_storage_public? %>" data-submitter="<%= submitter.to_json(only: %i[uuid slug name phone email]) %>" data-can-send-email="<%= Accounts.can_send_emails?(Struct.new(:id).new(@submitter.submission.template.account_id)) %>" data-attachments="<%= data_attachments %>" data-fields="<%= data_fields %>" data-authenticity-token="<%= form_authenticity_token %>" data-values="<%= submitter.values.to_json %>" data-with-typed-signature="<%= configs[:with_typed_signature] %>"></submission-form>
|
<submission-form data-is-demo="<%= Docuseal.demo? %>" data-completed-button="<%= configs[:completed_button].to_json %>" data-go-to-last="<%= submitter.opened_at? %>" data-is-direct-upload="<%= Docuseal.active_storage_public? %>" data-submitter="<%= submitter.to_json(only: %i[uuid slug name phone email]) %>" data-can-send-email="<%= Accounts.can_send_emails?(Struct.new(:id).new(@submitter.submission.template.account_id)) %>" data-attachments="<%= data_attachments %>" data-fields="<%= data_fields %>" data-values="<%= submitter.values.to_json %>" data-with-typed-signature="<%= configs[:with_typed_signature] %>"></submission-form>
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ Devise.otp_allowed_drift = 60.seconds
|
|||||||
#
|
#
|
||||||
# Use this hook to configure devise mailer, warden hooks and so forth.
|
# Use this hook to configure devise mailer, warden hooks and so forth.
|
||||||
# Many of these configuration options can be set straight in your model.
|
# Many of these configuration options can be set straight in your model.
|
||||||
# rubocop:disable Metrics/BlockLength
|
|
||||||
Devise.setup do |config|
|
Devise.setup do |config|
|
||||||
config.warden do |manager|
|
config.warden do |manager|
|
||||||
manager.default_strategies(scope: :user).unshift(:two_factor_authenticatable)
|
manager.default_strategies(scope: :user).unshift(:two_factor_authenticatable)
|
||||||
@@ -274,17 +273,6 @@ Devise.setup do |config|
|
|||||||
# The default HTTP method used to sign out a resource. Default is :delete.
|
# The default HTTP method used to sign out a resource. Default is :delete.
|
||||||
config.sign_out_via = :delete
|
config.sign_out_via = :delete
|
||||||
|
|
||||||
# ==> OmniAuth
|
|
||||||
# Add a new OmniAuth provider. Check the wiki for more information on setting
|
|
||||||
# up on your models and hooks.
|
|
||||||
config.omniauth :google_oauth2, ENV.fetch('GOOGLE_CLIENT_ID', nil), ENV.fetch('GOOGLE_CLIENT_SECRET', nil), {}
|
|
||||||
config.omniauth :microsoft_office365, ENV.fetch('OFFICE365_CLIENT_ID', nil),
|
|
||||||
ENV.fetch('OFFICE365_CLIENT_SECRET', nil), {}
|
|
||||||
|
|
||||||
if ENV['GITHUB_CLIENT_ID']
|
|
||||||
config.omniauth :github, ENV.fetch('GITHUB_CLIENT_ID', nil), ENV.fetch('GITHUB_CLIENT_SECRET', nil), {}
|
|
||||||
end
|
|
||||||
|
|
||||||
# ==> Warden configuration
|
# ==> Warden configuration
|
||||||
# If you want to use other strategies, that are not supported by Devise, or
|
# If you want to use other strategies, that are not supported by Devise, or
|
||||||
# change the failure app, you can configure them inside the config.warden block.
|
# change the failure app, you can configure them inside the config.warden block.
|
||||||
@@ -322,5 +310,6 @@ Devise.setup do |config|
|
|||||||
# When set to false, does not sign a user in automatically after their password is
|
# When set to false, does not sign a user in automatically after their password is
|
||||||
# changed. Defaults to true, so a user is signed in automatically after changing a password.
|
# changed. Defaults to true, so a user is signed in automatically after changing a password.
|
||||||
# config.sign_in_after_change_password = true
|
# config.sign_in_after_change_password = true
|
||||||
|
|
||||||
|
ActiveSupport.run_load_hooks(:devise_config, config)
|
||||||
end
|
end
|
||||||
# rubocop:enable Metrics/BlockLength
|
|
||||||
|
|||||||
+1
-1
@@ -10,7 +10,7 @@ Rails.application.routes.draw do
|
|||||||
path: '/', only: %i[sessions passwords omniauth_callbacks],
|
path: '/', only: %i[sessions passwords omniauth_callbacks],
|
||||||
controllers: begin
|
controllers: begin
|
||||||
options = { sessions: 'sessions', passwords: 'passwords' }
|
options = { sessions: 'sessions', passwords: 'passwords' }
|
||||||
options[:omniauth_callbacks] = 'omniauth_callbacks' if Docuseal.multitenant?
|
options[:omniauth_callbacks] = 'omniauth_callbacks' if User.devise_modules.include?(:omniauthable)
|
||||||
options
|
options
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,12 @@ module Submissions
|
|||||||
VERIFIED_TEXT = 'Verified'
|
VERIFIED_TEXT = 'Verified'
|
||||||
UNVERIFIED_TEXT = 'Unverified'
|
UNVERIFIED_TEXT = 'Unverified'
|
||||||
|
|
||||||
|
CURRENCY_SYMBOLS = {
|
||||||
|
'USD' => '$',
|
||||||
|
'EUR' => '€',
|
||||||
|
'GBP' => '£'
|
||||||
|
}.freeze
|
||||||
|
|
||||||
module_function
|
module_function
|
||||||
|
|
||||||
# rubocop:disable Metrics
|
# rubocop:disable Metrics
|
||||||
@@ -200,7 +206,15 @@ module Submissions
|
|||||||
|
|
||||||
composer.image(io, width:, height:, margin: [0, 0, 10, 0])
|
composer.image(io, width:, height:, margin: [0, 0, 10, 0])
|
||||||
composer.formatted_text_box([{ text: '' }])
|
composer.formatted_text_box([{ text: '' }])
|
||||||
elsif field['type'] == 'file'
|
elsif field['type'].in?(%w[file payment])
|
||||||
|
if field['type'] == 'payment'
|
||||||
|
unit = CURRENCY_SYMBOLS[field['preferences']['currency']]
|
||||||
|
|
||||||
|
price = ApplicationController.helpers.number_to_currency(field['preferences']['price'], unit:)
|
||||||
|
|
||||||
|
composer.formatted_text_box([{ text: "Paid #{price}\n" }], padding: [0, 0, 10, 0])
|
||||||
|
end
|
||||||
|
|
||||||
composer.formatted_text_box(
|
composer.formatted_text_box(
|
||||||
Array.wrap(value).map do |uuid|
|
Array.wrap(value).map do |uuid|
|
||||||
attachment = submitter.attachments.find { |a| a.uuid == uuid }
|
attachment = submitter.attachments.find { |a| a.uuid == uuid }
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ module Submissions
|
|||||||
width: image.width * scale,
|
width: image.width * scale,
|
||||||
height: image.height * scale
|
height: image.height * scale
|
||||||
)
|
)
|
||||||
when 'file'
|
when 'file', 'payment'
|
||||||
items = Array.wrap(value).each_with_object([]) do |uuid, acc|
|
items = Array.wrap(value).each_with_object([]) do |uuid, acc|
|
||||||
attachment = submitter.attachments.find { |a| a.uuid == uuid }
|
attachment = submitter.attachments.find { |a| a.uuid == uuid }
|
||||||
|
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ module Submitters
|
|||||||
def replace_default_variables(value, attrs, template, with_time: false)
|
def replace_default_variables(value, attrs, template, with_time: false)
|
||||||
return if value.blank?
|
return if value.blank?
|
||||||
|
|
||||||
value.gsub(VARIABLE_REGEXP) do |e|
|
value.to_s.gsub(VARIABLE_REGEXP) do |e|
|
||||||
case key = ::Regexp.last_match(1)
|
case key = ::Regexp.last_match(1)
|
||||||
when 'time'
|
when 'time'
|
||||||
if with_time
|
if with_time
|
||||||
|
|||||||
Reference in New Issue
Block a user