Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F1842164
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
45 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/src/resources/js/utils.js b/src/resources/js/utils.js
index 17312f73..9ca700c0 100644
--- a/src/resources/js/utils.js
+++ b/src/resources/js/utils.js
@@ -1,134 +1,187 @@
/**
* Clear (bootstrap) form validation state
*/
const clearFormValidation = (form) => {
$(form).find('.is-invalid').removeClass('is-invalid')
$(form).find('.invalid-feedback').remove()
}
/**
* File downloader
*/
const downloadFile = (url, filename) => {
// TODO: This might not be a best way for big files as the content
// will be stored (temporarily) in browser memory
// TODO: This method does not show the download progress in the browser
// but it could be implemented in the UI, axios has 'progress' property
axios.get(url, { responseType: 'blob' })
.then(response => {
const link = document.createElement('a')
if (!filename) {
const contentDisposition = response.headers['content-disposition']
filename = 'unknown'
if (contentDisposition) {
const match = contentDisposition.match(/filename="?(.+)"?/);
if (match && match.length === 2) {
filename = match[1];
}
}
}
link.href = window.URL.createObjectURL(response.data)
link.download = filename
link.click()
})
}
/**
* Create an object copy with specified properties only
*/
const pick = (obj, properties) => {
let result = {}
properties.forEach(prop => {
if (prop in obj) {
result[prop] = obj[prop]
}
})
return result
}
const loader = '<div class="app-loader"><div class="spinner-border" role="status"><span class="visually-hidden">Loading</span></div></div>'
let isLoading = 0
/**
* Display the 'loading...' element, lock the UI
*
* @param array|string|DOMElement|null|bool|jQuery $element Supported input:
* - DOMElement or jQuery collection or selector string: for element-level loader inside
* - array: for element-level loader inside the element specified in the first array element
* - undefined, null or true: for page-level loader
* @param object $style Additional element style
*/
const startLoading = (element, style = null) => {
let small = false
if (Array.isArray(element)) {
style = element[1]
element = element[0]
}
if (element && element !== true) {
// The loader inside some page element
small = true
if (style) {
small = style.small
delete style.small
$(element).css(style)
} else {
$(element).css('position', 'relative')
}
} else {
// The full page loader
isLoading++
let loading = $('#app > .app-loader').removeClass('fadeOut')
if (loading.length) {
return
}
element = $('#app')
}
const loaderElement = $(loader)
if (small) {
loaderElement.addClass('small')
}
$(element).append(loaderElement)
return loaderElement
}
/**
* Hide the "loading" element
*
* @param array|string|DOMElement|null|bool|jQuery $element
* @see startLoading()
*/
const stopLoading = (element) => {
if (element && element !== true) {
if (Array.isArray(element)) {
element = element[0]
}
$(element).find('.app-loader').remove()
} else if (isLoading > 0) {
$('#app > .app-loader').addClass('fadeOut')
isLoading--;
}
}
+let stripe = null
+
+const stripeInit = (callback) => {
+ let script = $('#stripe-script')
+
+ if (!script.length) {
+ script = document.createElement('script')
+
+ script.id = 'stripe-script'
+ script.src = 'https://js.stripe.com/v3/'
+ script.onload = () => {
+ stripe = Stripe(window.config.stripePK)
+ callback()
+ }
+
+ document.getElementsByTagName('head')[0].appendChild(script)
+ } else {
+ stripe = Stripe(window.config.stripePK)
+ callback()
+ }
+}
+
+/**
+ * Executes payment checkout.
+ *
+ * @param object Vue component object
+ * @param array Payment request parameters (Response from the payments API)
+ *
+ * @return bool Returns false if no supported checkout method is requested, True otherwise
+ */
+const paymentCheckout = (component, data) => {
+ if (data.redirectUrl) {
+ location.href = data.redirectUrl
+ } else if (data.newWindowUrl) {
+ window.open(data.newWindowUrl, '_blank')
+ } else if (data.id) {
+ stripeInit(() => {
+ stripe.redirectToCheckout({ sessionId: data.id }).then(result => {
+ // If it fails due to a browser or network error,
+ // display the localized error message to the user
+ if (result.error) {
+ component.$toast.error(result.error.message)
+ }
+ })
+ })
+ } else {
+ return false
+ }
+
+ return true
+}
+
export {
clearFormValidation,
downloadFile,
+ paymentCheckout,
pick,
startLoading,
stopLoading
}
diff --git a/src/resources/vue/Signup.vue b/src/resources/vue/Signup.vue
index c9d81c71..1fec7a99 100644
--- a/src/resources/vue/Signup.vue
+++ b/src/resources/vue/Signup.vue
@@ -1,437 +1,432 @@
<template>
<div class="container">
<div id="step0" v-if="!invitation">
<div class="plan-selector row row-cols-sm-2 g-3">
<div v-for="item in plans" :key="item.id" :id="'plan-' + item.title">
<div :class="'card bg-light plan-' + item.title">
<div class="card-header plan-header">
<div class="plan-ico text-center">
<svg-icon :icon="plan_icons[item.title] || 'user'"></svg-icon>
</div>
</div>
<div class="card-body text-center">
<btn class="btn-primary" :data-title="item.title" @click="selectPlan(item.title)" v-html="item.button"></btn>
<div class="plan-description text-start mt-3" v-html="item.description"></div>
</div>
</div>
</div>
</div>
</div>
<div class="card d-none" id="step1" v-if="!invitation">
<div class="card-body">
<h4 class="card-title">{{ $t('signup.title') }} - {{ $t('nav.step', { i: 1, n: steps }) }}</h4>
<p class="card-text">
{{ $t('signup.step1') }}
</p>
<form @submit.prevent="submitStep1" data-validation-prefix="signup_">
<div class="mb-3">
<div class="input-group">
<input type="text" class="form-control" id="signup_first_name" :placeholder="$t('form.firstname')" autofocus v-model="first_name">
<input type="text" class="form-control rounded-end" id="signup_last_name" :placeholder="$t('form.surname')" v-model="last_name">
</div>
</div>
<div v-if="mode == 'token'" class="mb-3">
<label for="signup_token" class="visually-hidden">{{ $t('signup.token') }}</label>
<input type="text" class="form-control" id="signup_token" :placeholder="$t('signup.token')" required v-model="token">
</div>
<div v-else class="mb-3">
<label for="signup_email" class="visually-hidden">{{ $t('signup.email') }}</label>
<input type="text" class="form-control" id="signup_email" :placeholder="$t('signup.email')" required v-model="email">
</div>
<btn class="btn-secondary" @click="stepBack">{{ $t('btn.back') }}</btn>
<btn class="btn-primary ms-2" type="submit" icon="check">{{ $t('btn.continue') }}</btn>
</form>
</div>
</div>
<div class="card d-none" id="step2" v-if="!invitation">
<div class="card-body">
<h4 class="card-title">{{ $t('signup.title') }} - {{ $t('nav.step', { i: 2, n: steps }) }}</h4>
<p class="card-text">
{{ $t('signup.step2') }}
</p>
<form @submit.prevent="submitStep2" data-validation-prefix="signup_">
<div class="mb-3">
<label for="signup_short_code" class="visually-hidden">{{ $t('form.code') }}</label>
<input type="text" class="form-control" id="signup_short_code" :placeholder="$t('form.code')" required v-model="short_code">
</div>
<btn class="btn-secondary" @click="stepBack">{{ $t('btn.back') }}</btn>
<btn class="btn-primary ms-2" type="submit" icon="check">{{ $t('btn.continue') }}</btn>
<input type="hidden" id="signup_code" v-model="code" />
</form>
</div>
</div>
<div class="card d-none" id="step3">
<div class="card-body">
<h4 v-if="!invitation && steps > 1" class="card-title">{{ $t('signup.title') }} - {{ $t('nav.step', { i: steps, n: steps }) }}</h4>
<p class="card-text">
{{ $t('signup.step3', { app: $root.appName }) }}
</p>
<form @submit.prevent="submitStep3" data-validation-prefix="signup_">
<div class="mb-3" v-if="invitation">
<div class="input-group">
<input type="text" class="form-control" id="signup_first_name" :placeholder="$t('form.firstname')" autofocus v-model="first_name">
<input type="text" class="form-control rounded-end" id="signup_last_name" :placeholder="$t('form.surname')" v-model="last_name">
</div>
</div>
<div class="mb-3">
<label for="signup_login" class="visually-hidden"></label>
<div class="input-group">
<input type="text" class="form-control" id="signup_login" required v-model="login" :placeholder="$t('signup.login')">
<span class="input-group-text">@</span>
<input v-if="is_domain" type="text" class="form-control rounded-end" id="signup_domain" required v-model="domain" :placeholder="$t('form.domain')">
<select v-else class="form-select rounded-end" id="signup_domain" required v-model="domain">
<option v-for="_domain in domains" :key="_domain" :value="_domain">{{ _domain }}</option>
</select>
</div>
</div>
<password-input class="mb-3" v-model="pass"></password-input>
<div class="mb-3">
<label for="signup_voucher" class="visually-hidden">{{ $t('signup.voucher') }}</label>
<input type="text" class="form-control" id="signup_voucher" :placeholder="$t('signup.voucher')" v-model="voucher">
</div>
<btn v-if="!invitation" class="btn-secondary me-2" @click="stepBack">{{ $t('btn.back') }}</btn>
<btn class="btn-primary" type="submit" icon="check">
<span v-if="invitation">{{ $t('btn.signup') }}</span>
<span v-else>{{ $t('btn.submit') }}</span>
</btn>
</form>
</div>
</div>
<div class="card d-none border-0" id="step4">
<div v-if="checkout.cost" class="card-body row row-cols-lg-2 align-items-center">
<h4 class="card-title text-center mb-4 col-lg-5">{{ $t('signup.created') }}</h4>
<div class="card-text mb-4 col-lg-7">
<div class="card internal" id="summary">
<div class="card-body">
<div class="card-text">
<h5>{{ checkout.title }}</h5>
<p id="summary-content">{{ checkout.content }}</p>
<p class="credit-cards">
<img src="/themes/default/images/visa.svg" alt="Visa" />
<img src="/themes/default/images/mastercard.svg" alt="Mastercard" />
</p>
<div id="summary-summary" class="mb-4" v-if="checkout.summary" v-html="checkout.summary"></div>
<form>
<btn class="btn-secondary me-2" @click="stepBack">{{ $t('btn.back') }}</btn>
<btn class="btn-primary" @click="submitStep4">{{ $t('btn.subscribe') }}</btn>
</form>
</div>
</div>
</div>
</div>
</div>
<div v-else class="card-body">
<h4 class="card-title mb-4">{{ $t('signup.created') }}</h4>
<div class="card-text mb-4" id="summary">
<p id="summary-content">{{ checkout.content }}</p>
<form>
- <btn class="btn-secondary me-2" @click="stepBack">{{ $t('btn.back') }}</btn>
- <btn class="btn-primary" @click="submitStep4">{{ $t('btn.subscribe') }}</btn>
+ <btn class="btn-secondary me-2" @click="stepBack">{{ $t('btn.back') }}</btn>
+ <btn class="btn-primary" @click="submitStep4">{{ $t('btn.subscribe') }}</btn>
</form>
</div>
</div>
</div>
</div>
</template>
<script>
import PasswordInput from './Widgets/PasswordInput'
+ import { paymentCheckout } from '../js/utils'
import { library } from '@fortawesome/fontawesome-svg-core'
library.add(
require('@fortawesome/free-solid-svg-icons/faMobileRetro').definition,
require('@fortawesome/free-solid-svg-icons/faUsers').definition
)
export default {
components: {
PasswordInput
},
data() {
return {
checkout: {},
email: '',
first_name: '',
last_name: '',
code: '',
short_code: '',
login: '',
pass: {},
domain: '',
domains: [],
invitation: null,
is_domain: false,
mode: 'email',
plan: null,
plan_icons: {
individual: 'user',
group: 'users',
phone: 'mobile-retro'
},
plans: [],
token: '',
voucher: ''
}
},
computed: {
steps() {
switch (this.mode) {
case 'token':
return 2
case 'mandate':
return 1
case 'email':
default:
return 3
}
}
},
mounted() {
let param = this.$route.params.param;
if (this.$route.name == 'signup-invite') {
axios.get('/api/auth/signup/invitations/' + param, { loader: true })
.then(response => {
this.invitation = response.data
this.login = response.data.login
this.voucher = response.data.voucher
this.first_name = response.data.first_name
this.last_name = response.data.last_name
this.plan = response.data.plan
this.is_domain = response.data.is_domain
this.setDomain(response.data)
this.displayForm(3, true)
})
.catch(error => {
this.$root.errorHandler(error)
})
} else if (param) {
if (this.$route.path.indexOf('/signup/voucher/') === 0) {
// Voucher (discount) code
this.voucher = param
this.displayForm(0)
} else if (/^([A-Z0-9]+)-([a-zA-Z0-9]+)$/.test(param)) {
// Verification code provided, auto-submit Step 2
this.short_code = RegExp.$1
this.code = RegExp.$2
this.submitStep2(true)
} else if (/^([a-zA-Z_]+)$/.test(param)) {
// Plan title provided, save it and display Step 1
this.step0(param)
} else {
this.$root.errorPage(404)
}
} else {
this.displayForm(0)
}
},
methods: {
selectPlan(plan) {
this.$router.push({path: '/signup/' + plan})
this.selectPlanByTitle(plan)
},
// Composes plan selection page
selectPlanByTitle(title) {
const plan = this.plans.filter(plan => plan.title == title)[0]
if (plan) {
this.plan = title
this.mode = plan.mode
this.is_domain = plan.isDomain
this.domain = ''
let step = 1
if (plan.mode == 'mandate') {
step = 3
if (!plan.isDomain || !this.domains.length) {
axios.get('/api/auth/signup/domains')
.then(response => {
this.displayForm(step, true)
this.setDomain(response.data)
})
return
}
}
this.displayForm(step, true)
}
},
step0(plan) {
if (!this.plans.length) {
axios.get('/api/auth/signup/plans', { loader: true }).then(response => {
this.plans = response.data.plans
this.selectPlanByTitle(plan)
})
.catch(error => {
this.$root.errorHandler(error)
})
} else {
this.selectPlanByTitle(plan)
}
},
// Submits data to the API, validates and gets verification code
submitStep1() {
this.$root.clearFormValidation($('#step1 form'))
const post = this.$root.pick(this, ['email', 'last_name', 'first_name', 'plan', 'token', 'voucher'])
axios.post('/api/auth/signup/init', post)
.then(response => {
this.code = response.data.code
this.short_code = response.data.short_code
this.mode = response.data.mode
this.is_domain = response.data.is_domain
this.displayForm(this.mode == 'token' ? 3 : 2, true)
// Fill the domain selector with available domains
if (!this.is_domain) {
this.setDomain(response.data)
}
})
},
// Submits the code to the API for verification
submitStep2(bylink) {
if (bylink === true) {
this.displayForm(2, false)
}
this.$root.clearFormValidation($('#step2 form'))
const post = this.$root.pick(this, ['code', 'short_code'])
axios.post('/api/auth/signup/verify', post)
.then(response => {
this.displayForm(3, true)
// Reset user name/email/plan, we don't have them if user used a verification link
this.first_name = response.data.first_name
this.last_name = response.data.last_name
this.email = response.data.email
this.is_domain = response.data.is_domain
this.voucher = response.data.voucher
this.domain = ''
// Fill the domain selector with available domains
if (!this.is_domain) {
this.setDomain(response.data)
}
})
.catch(error => {
if (bylink === true) {
// FIXME: display step 1, user can do nothing about it anyway
// Maybe we should display 404 error page?
this.displayForm(1, true)
}
})
},
// Submits the data to the API to create the user account
submitStep3() {
this.$root.clearFormValidation($('#step3 form'))
const post = this.lastStepPostData()
if (this.mode == 'mandate') {
axios.post('/api/auth/signup/validate', post).then(response => {
this.checkout = response.data
this.displayForm(4)
})
} else {
axios.post('/api/auth/signup', post).then(response => {
// auto-login and goto dashboard
this.$root.loginUser(response.data)
})
}
},
submitStep4() {
const post = this.lastStepPostData()
axios.post('/api/auth/signup', post).then(response => {
let checkout = response.data.checkout
// auto-login and goto to the payment checkout (or Dashboard for a free account)
- this.$root.loginUser(response.data, !checkout.id && !checkout.redirectUrl)
-
- if (checkout.redirectUrl) {
- location.href = checkout.redirectUrl
- } else if (checkout.id) {
- // TODO: this.stripeCheckout(checkout)
- }
+ this.$root.loginUser(response.data, !paymentCheckout(this, checkout))
})
},
// Moves the user a step back in registration form
stepBack(e) {
const card = $(e.target).closest('.card[id^="step"]')
let step = card.attr('id').replace('step', '')
card.addClass('d-none').find('form')[0].reset()
step -= 1
if (step == 2 && this.mode == 'token') {
step = 1
}
if (this.mode == 'mandate' && step < 3) {
step = 0
}
$('#step' + step).removeClass('d-none').find('input').first().focus()
if (!step) {
this.step0()
this.$router.replace({path: '/signup'})
}
},
displayForm(step, focus) {
[0, 1, 2, 3, 4].filter(value => value != step).forEach(value => {
$('#step' + value).addClass('d-none')
})
if (!step) {
return this.step0()
}
$('#step' + step).removeClass('d-none').find('form')[0].reset()
if (focus) {
$('#step' + step).find('input').first().focus()
}
},
lastStepPostData() {
let post = {
...this.$root.pick(this, ['login', 'domain', 'voucher', 'plan']),
...this.pass
}
if (this.invitation) {
post.invitation = this.invitation.id
post.first_name = this.first_name
post.last_name = this.last_name
} else {
post.code = this.code
post.short_code = this.short_code
}
return post
},
setDomain(response) {
if (response.domains) {
this.domains = response.domains
}
this.domain = response.domain
if (!this.domain) {
this.domain = window.config['app.domain']
if (this.domains.length && !this.domains.includes(this.domain)) {
this.domain = this.domains[0]
}
}
}
}
}
</script>
diff --git a/src/resources/vue/Wallet.vue b/src/resources/vue/Wallet.vue
index a1f534dc..5105f485 100644
--- a/src/resources/vue/Wallet.vue
+++ b/src/resources/vue/Wallet.vue
@@ -1,432 +1,384 @@
<template>
<div class="container" dusk="wallet-component">
<div v-if="wallet.id" id="wallet" class="card">
<div class="card-body">
<div class="card-title">{{ $t('wallet.title') }} <span :class="wallet.balance < 0 ? 'text-danger' : 'text-success'">{{ $root.price(wallet.balance, wallet.currency) }}</span></div>
<div class="card-text">
<p v-if="wallet.notice" id="wallet-notice">{{ wallet.notice }}</p>
<div v-if="showPendingPayments" class="alert alert-warning">
{{ $t('wallet.pending-payments-warning') }}
</div>
<p v-if="$root.hasPermission('walletPayments')">
<btn class="btn-primary" @click="paymentMethodForm('manual')">{{ $t('wallet.add-credit') }}</btn>
</p>
<div id="mandate-form" v-if="!mandate.isValid && !mandate.isPending && $root.hasPermission('walletMandates')">
<template v-if="mandate.id && !mandate.isValid">
<div class="alert alert-danger">
{{ $t('wallet.auto-payment-failed') }}
</div>
<btn class="btn-danger" @click="autoPaymentDelete">{{ $t('wallet.auto-payment-cancel') }}</btn>
</template>
<btn class="btn-primary" @click="paymentMethodForm('auto')">{{ $t('wallet.auto-payment-setup') }}</btn>
</div>
<div id="mandate-info" v-else-if="$root.hasPermission('walletMandates')">
<div v-if="mandate.isDisabled" class="disabled-mandate alert alert-danger">
{{ $t('wallet.auto-payment-disabled') }}
</div>
<template v-else>
<p v-html="$t('wallet.auto-payment-info', { amount: mandate.amount + ' ' + wallet.currency, balance: mandate.balance + ' ' + wallet.currency})"></p>
<p>{{ $t('wallet.payment-method', { method: mandate.method }) }}</p>
</template>
<div v-if="mandate.isPending" class="alert alert-warning">
{{ $t('wallet.auto-payment-inprogress') }}
</div>
<p class="buttons">
<btn class="btn-danger" @click="autoPaymentDelete">{{ $t('wallet.auto-payment-cancel') }}</btn>
<btn class="btn-primary" @click="autoPaymentChange">{{ $t('wallet.auto-payment-change') }}</btn>
</p>
</div>
</div>
</div>
</div>
<tabs class="mt-3" ref="tabs" :tabs="tabs"></tabs>
<div class="tab-content">
<div class="tab-pane active" id="receipts" role="tabpanel" aria-labelledby="tab-receipts">
<div class="card-body">
<div class="card-text">
<p v-if="receipts.length">
{{ $t('wallet.receipts-hint') }}
</p>
<div v-if="receipts.length" class="input-group">
<select id="receipt-id" class="form-control">
<option v-for="(receipt, index) in receipts" :key="index" :value="receipt">{{ receipt }}</option>
</select>
<btn class="btn-secondary" @click="receiptDownload" icon="download">{{ $t('btn.download') }}</btn>
</div>
<p v-if="!receipts.length">
{{ $t('wallet.receipts-none') }}
</p>
</div>
</div>
</div>
<div class="tab-pane" id="history" role="tabpanel" aria-labelledby="tab-history">
<div class="card-body">
<transaction-log v-if="walletId && loadTransactions" class="card-text" :wallet-id="walletId"></transaction-log>
</div>
</div>
<div class="tab-pane" id="payments" role="tabpanel" aria-labelledby="tab-payments">
<div class="card-body">
<payment-log v-if="walletId && loadPayments" class="card-text" :wallet-id="walletId"></payment-log>
</div>
</div>
</div>
<modal-dialog id="payment-dialog" ref="paymentDialog" :title="paymentDialogTitle" @click="payment" :buttons="dialogButtons">
<div id="payment-method" v-if="paymentForm == 'method'">
<form data-validation-prefix="mandate_">
<div id="payment-method-selection">
<a v-for="method in paymentMethods" :key="method.id" @click="selectPaymentMethod(method)" href="#" :class="'card link-' + method.id">
<svg-icon v-if="method.icon" :icon="[method.icon.prefix, method.icon.name]" />
<img v-if="method.image" :src="method.image" />
<span class="name">{{ method.name }}</span>
</a>
</div>
</form>
</div>
<div id="manual-payment" v-if="paymentForm == 'manual'">
<p v-if="wallet.currency != selectedPaymentMethod.currency && selectedPaymentMethod.id != 'bitcoin'">
{{ $t('wallet.currency-conv', { wc: wallet.currency, pc: selectedPaymentMethod.currency }) }}
</p>
<p v-if="selectedPaymentMethod.id == 'bitcoin'">
{{ $t('wallet.coinbase-hint', { wc: wallet.currency }) }}
</p>
<p v-if="selectedPaymentMethod.id == 'banktransfer'">
{{ $t('wallet.banktransfer-hint') }}
</p>
<p>
{{ $t('wallet.payment-amount-hint') }}
</p>
<form id="payment-form" @submit.prevent="payment">
<div class="input-group">
<input type="text" class="form-control" id="amount" v-model="amount" required>
<span class="input-group-text">{{ wallet.currency }}</span>
</div>
<div v-if="wallet.currency != selectedPaymentMethod.currency && !isNaN(amount) && selectedPaymentMethod.exchangeRate" class="alert alert-warning m-0 mt-3">
{{ $t('wallet.payment-warning', { price: $root.price(amount * selectedPaymentMethod.exchangeRate * 100, selectedPaymentMethod.currency) }) }}
</div>
</form>
<div class="alert alert-warning m-0 mt-3">
{{ $t('wallet.norefund') }}
</div>
</div>
<div id="auto-payment" v-if="paymentForm == 'auto'">
<form data-validation-prefix="mandate_">
<p>
{{ $t('wallet.auto-payment-hint') }}
</p>
<div class="row mb-3">
<label for="mandate_amount" class="col-sm-6 col-form-label">{{ $t('wallet.fill-up') }}</label>
<div class="col-sm-6">
<div class="input-group">
<input type="text" class="form-control" id="mandate_amount" v-model="mandate.amount" required>
<span class="input-group-text">{{ wallet.currency }}</span>
</div>
</div>
</div>
<div class="row mb-3">
<label for="mandate_balance" class="col-sm-6 col-form-label">{{ $t('wallet.when-below') }}</label>
<div class="col-sm-6">
<div class="input-group">
<input type="text" class="form-control" id="mandate_balance" v-model="mandate.balance" required>
<span class="input-group-text">{{ wallet.currency }}</span>
</div>
</div>
</div>
<p v-if="!mandate.isValid">
{{ $t('wallet.auto-payment-next') }}
</p>
<div v-if="mandate.isValid && mandate.isDisabled" class="disabled-mandate alert alert-danger m-0">
{{ $t('wallet.auto-payment-disabled-next') }}
</div>
</form>
<div class="alert alert-warning m-0 mt-3">
{{ $t('wallet.norefund') }}
</div>
</div>
</modal-dialog>
</div>
</template>
<script>
import ModalDialog from './Widgets/ModalDialog'
import TransactionLog from './Widgets/TransactionLog'
import PaymentLog from './Widgets/PaymentLog'
- import { downloadFile } from '../js/utils'
+ import { downloadFile, paymentCheckout } from '../js/utils'
import { library } from '@fortawesome/fontawesome-svg-core'
library.add(
require('@fortawesome/free-brands-svg-icons/faBitcoin').definition,
require('@fortawesome/free-solid-svg-icons/faBuildingColumns').definition,
require('@fortawesome/free-regular-svg-icons/faCreditCard').definition,
require('@fortawesome/free-solid-svg-icons/faDownload').definition,
require('@fortawesome/free-brands-svg-icons/faPaypal').definition
)
export default {
components: {
ModalDialog,
TransactionLog,
PaymentLog
},
data() {
return {
amount: '',
mandate: { amount: 10, balance: 0, method: null },
paymentDialogTitle: null,
paymentForm: null,
nextForm: null,
receipts: [],
- stripe: null,
loadTransactions: false,
loadPayments: false,
showPendingPayments: false,
wallet: {},
walletId: null,
paymentMethods: [],
selectedPaymentMethod: null
}
},
computed: {
dialogButtons() {
if (this.paymentForm == 'method') {
return []
}
const button = {
className: 'btn-primary modal-action',
icon: 'check',
label: 'btn.submit'
}
if (this.paymentForm == 'manual'
|| (this.paymentForm == 'auto' && !this.mandate.isValid && !this.mandate.isPending)
) {
button.label = 'btn.continue'
}
return [ button ]
},
tabs() {
let tabs = [ 'wallet.receipts', 'wallet.history' ]
if (this.showPendingPayments) {
tabs.push('wallet.pending-payments')
}
return tabs
}
},
mounted() {
$('#wallet button').focus()
this.walletId = this.$root.authInfo.wallets[0].id
axios.get('/api/v4/wallets/' + this.walletId, { loader: true })
.then(response => {
this.wallet = response.data
axios.get('/api/v4/wallets/' + this.walletId + '/receipts', { loader: '#receipts' })
.then(response => {
this.receipts = response.data.list
})
-
- if (this.wallet.provider == 'stripe') {
- this.stripeInit()
- }
})
.catch(this.$root.errorHandler)
this.loadMandate()
axios.get('/api/v4/payments/has-pending')
.then(response => {
this.showPendingPayments = response.data.hasPending
})
this.$refs.tabs.clickHandler('history', () => { this.loadTransactions = true })
this.$refs.tabs.clickHandler('payments', () => { this.loadPayments = true })
},
methods: {
loadMandate() {
const loader = '#mandate-form'
this.$root.stopLoading(loader)
axios.get('/api/v4/payments/mandate', { loader })
.then(response => {
this.mandate = response.data
if (this.mandate.minAmount) {
if (this.mandate.minAmount > this.mandate.amount) {
this.mandate.amount = this.mandate.minAmount
}
}
})
},
selectPaymentMethod(method) {
this.formLock = false
this.selectedPaymentMethod = method
this.paymentForm = this.nextForm
setTimeout(() => { $('#payment-dialog').find('#amount,#mandate_amount').focus() }, 10)
},
payment() {
if (this.paymentForm == 'auto') {
return this.autoPayment()
}
if (this.formLock) {
return
}
// Lock the form to prevent from double submission
this.formLock = true
let onFinish = () => { this.formLock = false }
this.$root.clearFormValidation($('#payment-form'))
const post = {
amount: this.amount,
methodId: this.selectedPaymentMethod.id,
currency: this.selectedPaymentMethod.currency
}
axios.post('/api/v4/payments', post, { onFinish })
.then(response => {
- if (response.data.redirectUrl) {
- location.href = response.data.redirectUrl
- } else if (response.data.newWindowUrl) {
- window.open(response.data.newWindowUrl, '_blank')
- this.$refs.paymentDialog.hide();
- } else {
- this.stripeCheckout(response.data)
- }
+ paymentCheckout(this, response.data)
+ this.$refs.paymentDialog.hide();
})
},
autoPayment() {
if (this.formLock) {
return
}
// Lock the form to prevent from double submission
this.formLock = true
let onFinish = () => { this.formLock = false }
const method = this.mandate.id && (this.mandate.isValid || this.mandate.isPending) ? 'put' : 'post'
let post = {
amount: this.mandate.amount,
balance: this.mandate.balance,
}
// Modifications can't change the method of payment
if (this.selectedPaymentMethod) {
post.methodId = this.selectedPaymentMethod.id
post.currency = this.selectedPaymentMethod.currency
}
this.$root.clearFormValidation($('#auto-payment form'))
axios[method]('/api/v4/payments/mandate', post, { onFinish })
.then(response => {
if (method == 'post') {
this.mandate.id = null
// a new mandate, redirect to the chackout page
- if (response.data.redirectUrl) {
- location.href = response.data.redirectUrl
- } else if (response.data.id) {
- this.stripeCheckout(response.data)
- }
+ paymentCheckout(this, response.data)
} else {
// an update
if (response.data.status == 'success') {
this.$refs.paymentDialog.hide();
this.mandate = response.data
this.$toast.success(response.data.message)
}
}
})
},
autoPaymentChange(event) {
this.autoPaymentForm(event, this.$t('wallet.auto-payment-update'))
},
autoPaymentDelete() {
axios.delete('/api/v4/payments/mandate')
.then(response => {
this.mandate = { amount: 10, balance: 0 }
if (response.data.status == 'success') {
this.$toast.success(response.data.message)
}
})
},
paymentMethodForm(nextForm) {
this.formLock = false
this.paymentMethods = []
this.paymentForm = 'method'
this.nextForm = nextForm
this.paymentDialogTitle = this.$t(nextForm == 'auto' ? 'wallet.auto-payment-setup' : 'wallet.top-up')
this.$refs.paymentDialog.show()
this.$nextTick().then(() => {
const type = nextForm == 'manual' ? 'oneoff' : 'recurring'
const loader = ['#payment-method', { 'min-height': '10em', small: false }]
axios.get('/api/v4/payments/methods', { params: { type }, loader })
.then(response => {
this.paymentMethods = response.data
if (this.paymentMethods.length == 1) {
this.nextForm = 'auto';
this.selectPaymentMethod(this.paymentMethods[0]);
}
})
})
},
autoPaymentForm(event, title) {
this.paymentForm = 'auto'
this.paymentDialogTitle = title
this.formLock = false
this.$refs.paymentDialog.show()
},
receiptDownload() {
const receipt = $('#receipt-id').val()
downloadFile('/api/v4/wallets/' + this.walletId + '/receipts/' + receipt)
- },
- stripeInit() {
- let script = $('#stripe-script')
-
- if (!script.length) {
- script = document.createElement('script')
-
- script.onload = () => {
- this.stripe = Stripe(window.config.stripePK)
- }
-
- script.id = 'stripe-script'
- script.src = 'https://js.stripe.com/v3/'
-
- document.getElementsByTagName('head')[0].appendChild(script)
- } else {
- this.stripe = Stripe(window.config.stripePK)
- }
- },
- stripeCheckout(data) {
- if (!this.stripe) {
- return
- }
-
- this.stripe.redirectToCheckout({
- sessionId: data.id
- }).then(result => {
- // If it fails due to a browser or network error,
- // display the localized error message to the user
- if (result.error) {
- this.$toast.error(result.error.message)
- }
- })
}
}
}
</script>
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Mon, Aug 25, 8:23 PM (16 h, 25 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
257783
Default Alt Text
(45 KB)
Attached To
Mode
R2 kolab
Attached
Detach File
Event Timeline
Log In to Comment