Page MenuHomePhorge

No OneTemporary

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

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)

Event Timeline