feat: implement add stripe card feat: implement login process Merge branch 'master' of git.bubblev.org:bubblev/bubble-web into feat/ui-layout feat: implement payment page feat: implement mail verification screen Merge branch 'master' of git.bubblev.org:bubblev/bubble-web into feat/ui-layout feat: prevent registration when the configs.allowRegistration is set to false fix: weird routing Merge branch 'master' of git.bubblev.org:bubblev/bubble-web into feat/ui-layout Merge branch 'master' of git.bubblev.org:bubblev/bubble-web into feat/ui-layout feat: implement actual pages Merge branch 'master' of git.bubblev.org:bubblev/bubble-web into feat/ui-layout feat: api integration for register page fix: messages feat: api integration for forget-password page fix: login title and password placeholder feat: disable button until get response Merge branch 'master' of git.bubblev.org:bubblev/bubble-web into feat/ui-layout Merge branch 'master' of git.bubblev.org:bubblev/bubble-web into feat/ui-layout feat: integrate api for new login page feat: implement responsive header feat: implement checkbox Merge branch 'master' of git.bubblev.org:bubblev/bubble-web into feat/ui-layout fix: checkbox event issue feat: implement register page feat: implement registration page Merge branch 'master' of git.bubblev.org:bubblev/bubble-web into feat/ui-layout feat: implement UI for forgot-password Merge branch 'master' of git.bubblev.org:bubblev/bubble-web into feat/ui-layout feat: implement new login page feat: implement input shared component Merge branch 'master' of git.bubblev.org:bubblev/bubble-web into feat/ui-layout feat: implement auth header Merge branch 'feat/ui-layout' of git.bubblev.org:bubblev/bubble-web into feat/ui-layout capitalize filename fix: webpack config to add output default url Merge branch 'master' into feat/ui-layout Merge branch 'master' into feat/ui-layout feat: integrate lazy loading and new page structure feat: use different layout for new Pages Co-authored-by: Tyler <everdev0923@gmail.com> Co-authored-by: Jonathan Cobb <jonathan@kyuss.org> Co-authored-by: jonathan <jonathan@noreply.git.bubblev.org> Reviewed-on: https://git.bubblev.org/bubblev/bubble-web/pulls/28pull/32/head
@@ -7751,6 +7751,14 @@ | |||
} | |||
} | |||
}, | |||
"vue-lottie": { | |||
"version": "0.2.1", | |||
"resolved": "https://registry.npmjs.org/vue-lottie/-/vue-lottie-0.2.1.tgz", | |||
"integrity": "sha512-zInUX69Ij8MhVR3XArpu4PqqBoufwKxS5UMutWCPm59VUaB5H6GtnaIzf9M+l6aYU+Kr8gF/W9dzWLgRuU6V+Q==", | |||
"requires": { | |||
"lottie-web": "^5.1.9" | |||
} | |||
}, | |||
"vue-router": { | |||
"version": "3.0.6", | |||
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-3.0.6.tgz", | |||
@@ -1,6 +0,0 @@ | |||
{ | |||
"verify_email_title": "Please verify your email address", | |||
"resend_verify_email_label": "Did you not received the email?", | |||
"button_label_resend_verify_email": "Resend it.", | |||
"more_features_label": "More Features" | |||
} |
@@ -0,0 +1,7 @@ | |||
{ | |||
"payment_page_title": "Please add a payment method", | |||
"payment_page_sub_title": "We need a CC card to secure your place in the Bubblesphere.", | |||
"label_bubble_free_title": "Bubble is free for 30 days.", | |||
"label_bubble_free_description": "We think you'll love the security of being in your Bubble.<br/> If for any reason you want to cancel, that's easy too!" | |||
} |
@@ -0,0 +1,29 @@ | |||
{ | |||
"verify_email_title": "Please verify your email address", | |||
"resend_verify_email_label": "Did you not received the email?", | |||
"button_label_resend_verify_email": "Resend it.", | |||
"more_features_label": "More Features", | |||
"button_label_add_card": "Add Card", | |||
"label_pricing_option_format": "{{messages[`marketing_pricing_${plan}_title`]}}: {{messages.currency_symbol_USD}} {{price}} monthly (free for 30 days)", | |||
"marketing_pricing_options": "personal,power,mega", | |||
"marketing_pricing_personal_title": "Personal Bubble", | |||
"marketing_pricing_personal_users": "1 User Account", | |||
"marketing_pricing_personal_price": "1200", | |||
"marketing_pricing_personal_options": "1 User Account,1TB/Month of Data Transfer", | |||
"marketing_pricing_personal_link": "/register?plan=bubble", | |||
"marketing_pricing_power_title": "Power Plan", | |||
"marketing_pricing_power_users": "5 User Accounts", | |||
"marketing_pricing_power_price": "1900", | |||
"marketing_pricing_power_options": "5 User Accounts,2TB/Month of Data Transfer", | |||
"marketing_pricing_power_link": "/register?plan=bubble_plus", | |||
"marketing_pricing_mega_title": "Mega Plan", | |||
"marketing_pricing_mega_users": "10 User Accounts", | |||
"marketing_pricing_mega_price": "3100", | |||
"marketing_pricing_mega_options": "10 User Accounts,3TB/Month of Data Transfer", | |||
"marketing_pricing_mega_link": "/register?plan=bubble_super" | |||
} |
@@ -7,7 +7,7 @@ | |||
<div class="navbar d-none d-md-flex"> | |||
<!--- If not logged in ---> | |||
<div | |||
v-if="status.loggedIn === false" | |||
v-if="status.loggedIn !== true" | |||
class="d-flex justify-content-center align-items-center" | |||
> | |||
<router-link to="/help" class="d-flex align-items-center"> | |||
@@ -146,11 +146,9 @@ export default { | |||
methods: { | |||
toggleNavbar() { | |||
console.log('toggleNavbar'); | |||
this.menuVisible = !this.prevVisibleState; | |||
}, | |||
hide() { | |||
console.log('hide'); | |||
this.prevVisibleState = this.menuVisible; | |||
this.menuVisible = false; | |||
}, | |||
@@ -11,7 +11,7 @@ | |||
:key="index" | |||
class="col-lg-3 col-md-6 col-sm-12 my-4 px-3" | |||
> | |||
<Card> | |||
<Card class="h-100"> | |||
<div class="card-content"> | |||
<span | |||
class="card-icon" | |||
@@ -49,10 +49,12 @@ | |||
&.block { | |||
display: block; | |||
width: 100%; | |||
} | |||
.btn--text { | |||
text-transform: uppercase; | |||
white-space: nowrap; | |||
padding: 5px 20px; | |||
} | |||
@@ -1,6 +1,6 @@ | |||
<!-- Copyright (c) 2020 Bubble, Inc. All rights reserved. For personal (non-commercial) use, see license: https://getbubblenow.com/bubble-license/ --> | |||
<template> | |||
<div class="card-container h-100"> | |||
<div class="card-container" :class="{ 'rounded-corner': roundCorner }"> | |||
<slot></slot> | |||
</div> | |||
</template> | |||
@@ -9,8 +9,11 @@ | |||
.card-container { | |||
background-color: white; | |||
padding: 40px; | |||
border-radius: 10px; | |||
box-shadow: 0px 10px 50px #dee1ec; | |||
&.rounded-corner { | |||
border-radius: 10px; | |||
} | |||
} | |||
.card-content { | |||
@@ -47,5 +50,12 @@ | |||
</style> | |||
<script> | |||
export default {}; | |||
export default { | |||
props: { | |||
roundCorner: { | |||
type: Boolean, | |||
default: true, | |||
}, | |||
}, | |||
}; | |||
</script> |
@@ -8,7 +8,7 @@ | |||
{{ messages.forgot_password_blurb }} | |||
</h4> | |||
<form class="auth-form" @submit.prevent="handleSubmit"> | |||
<form class="bubble-form" @submit.prevent="handleSubmit"> | |||
<div class="form-group"> | |||
<Input | |||
class="form-control" | |||
@@ -30,7 +30,7 @@ | |||
</div> | |||
<Button | |||
color="default" | |||
class="auth-form-submit" | |||
class="bubble-form-submit" | |||
@click="handleSubmit" | |||
:disabled="status.sendingResetPasswordMessage" | |||
> | |||
@@ -42,13 +42,19 @@ export default { | |||
computed: { | |||
backgroundClass() { | |||
const urlNodes = this.$route.fullPath.split('/'); | |||
switch (urlNodes[1]) { | |||
case 'login': | |||
case 'forgotPassword': | |||
return 'background1'; | |||
} | |||
// new pages in progress | |||
switch (urlNodes[2]) { | |||
case 'login': | |||
case 'forgot-password': | |||
case 'forgotPassword': | |||
return 'background1'; | |||
default: | |||
return 'background2'; | |||
} | |||
return 'background2'; | |||
}, | |||
}, | |||
@@ -10,7 +10,7 @@ | |||
{{ messages.login_blurb }} | |||
</h4> | |||
<form class="auth-form" @submit.prevent="submit"> | |||
<form class="bubble-form" @submit.prevent="submit"> | |||
<h4 | |||
v-if="submitted && errors.has('approvalToken')" | |||
class="invalid-feedback d-block" | |||
@@ -131,8 +131,9 @@ | |||
{{ messages.button_label_forgotPassword }} | |||
</router-link> | |||
<Button | |||
block | |||
color="default" | |||
class="auth-form-submit" | |||
class="bubble-form-submit" | |||
@click="submit" | |||
:disabled="status.loggingIn" | |||
> | |||
@@ -8,7 +8,7 @@ | |||
{{ messages.register_blurb }} | |||
</h4> | |||
<form class="auth-form" @submit.prevent="handleSubmit"> | |||
<form class="bubble-form" @submit.prevent="handleSubmit"> | |||
<div class="form-group"> | |||
<Input | |||
class="form-control" | |||
@@ -157,43 +157,6 @@ | |||
> | |||
{{ messages.marketing_pricing_title }} | |||
</a> | |||
<!-- <div class="row px-5 mx-5"> | |||
<div class="col-12 d-flex"> | |||
<div | |||
class="plan flex-grow-1" | |||
v-for="(plan, index) in messages.marketing_pricing_options.split(',')" | |||
:key="index" | |||
> | |||
<p class="plan-name"> | |||
{{ messages[`marketing_pricing_${plan}_title`] }} | |||
</p> | |||
<p class="plan-users"> | |||
{{ messages[`marketing_pricing_${plan}_users`] }} | |||
</p> | |||
<p class="plan-pricing"> | |||
{{ messages[`marketing_pricing_${plan}_users`] }} | |||
</p> | |||
<p | |||
class="plan-common-features" | |||
v-for="option in messages[`marketing_pricing_common_options`].split( | |||
',' | |||
)" | |||
:key="option" | |||
> | |||
{{ option }} | |||
</p> | |||
<p | |||
class="plan-features" | |||
v-for="option in messages[ | |||
`marketing_pricing_${plan}_options` | |||
].split(',')" | |||
:key="option" | |||
> | |||
{{ option }} | |||
</p> | |||
</div> | |||
</div> | |||
</div> --> | |||
</div> | |||
</template> | |||
@@ -1,11 +1,156 @@ | |||
<!-- Copyright (c) 2020 Bubble, Inc. All rights reserved. For personal (non-commercial) use, see license: https://getbubblenow.com/bubble-license/ --> | |||
<template> | |||
<div> | |||
Main Layout | |||
<router-view></router-view> | |||
</div> | |||
<router-view></router-view> | |||
</template> | |||
<script> | |||
export default {}; | |||
import { mapState, mapActions } from 'vuex'; | |||
import { util } from '~/_helpers'; | |||
import { isAuthenticator, isNotAuthenticator } from '~/_store/users.module'; | |||
export default { | |||
data: () => ({ | |||
verifiedContacts: null, | |||
verifiedContactRefresher: null, | |||
accountPlan: { | |||
name: '', | |||
domain: '', | |||
locale: util.currentUser().locale, | |||
timezone: '', | |||
plan: 'bubble', | |||
footprint: 'Worldwide', | |||
paymentMethodObject: { | |||
uuid: null, | |||
paymentMethodType: null, | |||
paymentInfo: null, | |||
}, | |||
sshKey: '', | |||
forkHost: '', | |||
syncPassword: true, | |||
launchLock: false, | |||
sendErrors: true, | |||
sendMetrics: true, | |||
}, | |||
payMethods: null, | |||
}), | |||
computed: { | |||
...mapState('users', ['policy']), | |||
...mapState('paymentMethods', ['accountPaymentMethods']), | |||
}, | |||
mounted() { | |||
this.initDefaults(); | |||
}, | |||
methods: { | |||
...mapActions('users', ['getPolicyByUserId']), | |||
...mapActions('paymentMethods', ['getAllAccountPaymentMethods']), | |||
initDefaults() { | |||
const currentUser = util.currentUser(); | |||
const selectedLocale = | |||
currentUser !== null && | |||
typeof currentUser.locale !== 'undefined' && | |||
currentUser.locale !== null | |||
? currentUser.locale | |||
: 'detect'; | |||
this.getPolicyByUserId({ | |||
userId: currentUser.uuid, | |||
messages: this.messages, | |||
errors: this.errors, | |||
}); | |||
}, | |||
hasVerifiedContact(policy) { | |||
if (policy && policy.accountContacts) { | |||
const contacts = policy.accountContacts; | |||
for (let i = 0; i < contacts.length; i++) { | |||
if (contacts[i].verified && isNotAuthenticator(contacts[i])) | |||
return true; | |||
} | |||
return false; | |||
} | |||
return false; | |||
}, | |||
navigateToVerifyEmail() { | |||
if (this.$route.path !== '/new_pages/verify-email') { | |||
this.$router.push('/new_pages/verify-email'); | |||
} | |||
}, | |||
navigateToPaymentPage() { | |||
if (this.$route.path !== '/new_pages/payment') { | |||
this.$router.push('/new_pages/payment'); | |||
} | |||
}, | |||
navigateToDashboard() { | |||
if ( | |||
this.$route.path === '/new_pages/payment' || | |||
this.$route.path === '/new_pages/verify-email' | |||
) { | |||
this.$router.push('/new_pages/test'); | |||
} | |||
}, | |||
}, | |||
watch: { | |||
policy(p) { | |||
this.verifiedContacts = this.hasVerifiedContact(p); | |||
if (!this.verifiedContacts) { | |||
this.navigateToVerifyEmail(); | |||
if (this.verifiedContactRefresher === null) { | |||
const vue = this; | |||
const currentUser = util.currentUser(); | |||
this.verifiedContactRefresher = window.setInterval(() => { | |||
vue.getPolicyByUserId({ | |||
userId: currentUser.uuid, | |||
messages: vue.messages, | |||
errors: vue.errors, | |||
}); | |||
}, 5000); | |||
} | |||
} else { | |||
console.log('hasVerified Contact'); | |||
const currentUser = util.currentUser(); | |||
this.getAllAccountPaymentMethods({ | |||
userId: currentUser.uuid, | |||
messages: this.messages, | |||
errors: this.errors, | |||
}); | |||
if (this.verifiedContactRefresher !== null) { | |||
window.clearInterval(this.verifiedContactRefresher); | |||
this.verifiedContactRefresher = null; | |||
} | |||
} | |||
}, | |||
accountPaymentMethods(pms) { | |||
if (pms) { | |||
const payMethods = []; | |||
for (let i = 0; i < pms.length; i++) { | |||
const pm = pms[i]; | |||
if ( | |||
(typeof pm.promotion === 'undefined' || | |||
pm.promotion === null || | |||
!pm.promotion) && | |||
(typeof pm.deleted === 'undefined' || pm.deleted === null) | |||
) { | |||
payMethods.push(pm); | |||
} | |||
} | |||
if ( | |||
this.accountPlan.paymentMethodObject.uuid === null && | |||
payMethods.length > 0 | |||
) { | |||
this.navigateToDashboard(); | |||
} else { | |||
this.navigateToPaymentPage(); | |||
} | |||
} | |||
}, | |||
}, | |||
}; | |||
</script> |
@@ -0,0 +1,56 @@ | |||
<template> | |||
<div :class="backgroundClass" class="auth-layout d-flex flex-column content"> | |||
<router-view></router-view> | |||
</div> | |||
</template> | |||
<style lang="scss" scoped> | |||
@import '../../../_scss/breakpoints'; | |||
.content { | |||
background-repeat: no-repeat; | |||
background-size: 100% auto; | |||
background-position-x: center; | |||
padding: 80px; | |||
@include respond-below(sm) { | |||
background-size: 200%, auto; | |||
padding: 20px; | |||
} | |||
} | |||
.background1 { | |||
background-image: url('/background1.svg'); | |||
} | |||
.background2 { | |||
background-image: url('/background2.svg'); | |||
} | |||
</style> | |||
<script> | |||
import { Header } from '~/_components/layout'; | |||
export default { | |||
components: { | |||
Header, | |||
}, | |||
computed: { | |||
backgroundClass() { | |||
const urlNodes = this.$route.fullPath.split('/'); | |||
switch (urlNodes[1]) { | |||
case 'payment': | |||
return 'background1'; | |||
} | |||
// new pages in progress | |||
switch (urlNodes[2]) { | |||
case 'payment': | |||
return 'background1'; | |||
} | |||
return 'background2'; | |||
}, | |||
}, | |||
}; | |||
</script> |
@@ -0,0 +1,446 @@ | |||
<template> | |||
<div class="wrapper"> | |||
<h1 class="text-center white-text form-title"> | |||
{{ messages.payment_page_title }} | |||
</h1> | |||
<h4 | |||
class="d-flex align-items-center justify-content-center white-text form-sub-title" | |||
> | |||
{{ messages.payment_page_sub_title }} | |||
</h4> | |||
<form class="bubble-form" @submit.prevent="authorizeCard"> | |||
<select | |||
v-validate="'required'" | |||
v-if="planObjects" | |||
v-model="bubblePlan" | |||
name="plan" | |||
class="form-control" | |||
:class="{ 'is-invalid': submitted && errors.has('plan') }" | |||
> | |||
<option | |||
v-for="(plan, index) in planObjects" | |||
:value="plan.name" | |||
:key="index" | |||
> | |||
{{ messages['plan_name_' + plan.name] }} - | |||
{{ | |||
messages.price_format.parseExpression({ | |||
messages: messages, | |||
...plan, | |||
}) | |||
}} | |||
</option> | |||
</select> | |||
<div id="card-number" class="form-control mt-3" /> | |||
<div class="mt-3 d-flex"> | |||
<div id="card-expiry" class="form-control mr-1" /> | |||
<div id="card-cvc" class="form-control" /> | |||
<div class="flex-grow-1"></div> | |||
<div id="card-zip" class="form-control" /> | |||
</div> | |||
<p class="text-center mt-3"> | |||
<small> | |||
{{ messages['label_promotion_FirstMonthFree_description'] }} | |||
</small> | |||
</p> | |||
<p v-if="stripeError" class="invalid-feedback d-block"> | |||
{{ stripeError }} | |||
</p> | |||
<div | |||
v-if="submitted && errors.has('purchase')" | |||
class="invalid-feedback d-block" | |||
> | |||
{{ errors.first('purchase') }} | |||
</div> | |||
<div | |||
v-if="submitted && errors.has('paymentMethod')" | |||
class="invalid-feedback d-block" | |||
> | |||
{{ errors.first('paymentMethod') }} | |||
</div> | |||
<div | |||
v-if="submitted && errors.has('paymentMethodInfo')" | |||
class="invalid-feedback d-block" | |||
> | |||
{{ errors.first('paymentMethodInfo') }} | |||
</div> | |||
<div | |||
v-if="submitted && errors.has('paymentMethodType')" | |||
class="invalid-feedback d-block" | |||
> | |||
{{ errors.first('paymentMethodType') }} | |||
</div> | |||
<div | |||
v-if="submitted && errors.has('paymentMethodService')" | |||
class="invalid-feedback d-block" | |||
> | |||
{{ errors.first('paymentMethodService') }} | |||
</div> | |||
<div | |||
v-if="submitted && errors.has('paymentInfo')" | |||
class="invalid-feedback d-block" | |||
> | |||
{{ errors.first('paymentInfo') }} | |||
</div> | |||
<div | |||
v-if="submitted && errors.has('plan')" | |||
class="invalid-feedback d-block" | |||
> | |||
{{ errors.first('plan') }} | |||
</div> | |||
<Button | |||
block | |||
color="default" | |||
class="bubble-form-submit" | |||
@click="authorizeCard" | |||
v-if=" | |||
paymentStatus.addingPaymentMethod || !paymentStatus.addedPaymentMethod | |||
" | |||
:disabled="paymentStatus.addingPaymentMethod" | |||
> | |||
{{ messages.button_label_add_card }} | |||
</Button> | |||
</form> | |||
<!--- Pricing Section ---> | |||
<div class="mt-5"> | |||
<h2 class="text-center"> | |||
{{ messages.label_bubble_free_title }} | |||
</h2> | |||
<h5 | |||
class="text-center" | |||
v-html="messages.label_bubble_free_description" | |||
></h5> | |||
</div> | |||
<div | |||
class="row px-5 mx-5 mt-5" | |||
v-if="messages && messages.marketing_pricing_common_options" | |||
> | |||
<div class="col-12 d-flex plan-section"> | |||
<Card | |||
class="plan flex-grow-1" | |||
v-for="(plan, index) in messages.marketing_pricing_options.split(',')" | |||
:round-corner="false" | |||
:key="index" | |||
> | |||
<p class="plan-name"> | |||
{{ messages[`marketing_pricing_${plan}_title`] }} | |||
</p> | |||
<p class="plan-users"> | |||
{{ messages[`marketing_pricing_${plan}_users`] }} | |||
</p> | |||
<p class="plan-pricing"> | |||
<span class="symbol"> | |||
{{ messages.currency_symbol_USD }} | |||
</span> | |||
<span class="price"> | |||
{{ messages[`marketing_pricing_${plan}_price`].parsePrice() }} | |||
</span> | |||
<span class="period"> | |||
{{ messages.marketing_pricing_period }} | |||
</span> | |||
</p> | |||
<p | |||
class="plan-common-features text-center" | |||
v-for="option in messages.marketing_pricing_common_options.split( | |||
',' | |||
)" | |||
:key="option" | |||
> | |||
{{ option }} | |||
</p> | |||
<p | |||
class="plan-features text-center" | |||
v-for="option in messages[ | |||
`marketing_pricing_${plan}_options` | |||
].split(',')" | |||
:key="option" | |||
> | |||
{{ option }} | |||
</p> | |||
</Card> | |||
</div> | |||
</div> | |||
</div> | |||
</template> | |||
<style lang="scss" scoped> | |||
@import '../../../_scss/components/form'; | |||
.features-section-link { | |||
color: $vivid-navy; | |||
font-size: 16px; | |||
margin-top: 25px; | |||
} | |||
.plan-section { | |||
@include respond-below(md) { | |||
flex-direction: column; | |||
} | |||
} | |||
#card-expiry, | |||
#card-cvc, | |||
#card-zip { | |||
max-width: 25%; | |||
} | |||
.plan { | |||
display: flex; | |||
flex-direction: column; | |||
align-items: center; | |||
padding: 20px; | |||
background-color: white; | |||
&:nth-child(2n + 1) { | |||
margin-top: 30px; | |||
color: $vivid-navy; | |||
} | |||
&:nth-child(2n) { | |||
z-index: 1; | |||
color: $strong-purple-1; | |||
} | |||
.plan-users { | |||
color: inherit; | |||
font-weight: 700; | |||
font-size: 0.9em; | |||
} | |||
.plan-pricing { | |||
color: inherit; | |||
display: flex; | |||
margin: 2em 0; | |||
.symbol { | |||
font-size: 1.5em; | |||
} | |||
.price { | |||
font-size: 5em; | |||
line-height: 1em; | |||
font-weight: bold; | |||
} | |||
.period { | |||
font-size: 1.5em; | |||
align-self: flex-end; | |||
} | |||
} | |||
.plan-name { | |||
color: #2e2545; | |||
font-size: 1.5em; | |||
font-weight: 500; | |||
} | |||
.plan-common-features { | |||
color: #8585bd; | |||
} | |||
.plan-features { | |||
color: #8585bd; | |||
font-weight: bold; | |||
} | |||
} | |||
</style> | |||
<script> | |||
import { mapState, mapActions } from 'vuex'; | |||
import { util } from '~/_helpers'; | |||
import { Button, Card } from '~/_components/shared'; | |||
export default { | |||
components: { | |||
Button, | |||
Card, | |||
}, | |||
data() { | |||
return { | |||
user: util.currentUser(), | |||
bubblePlan: '', | |||
card: { | |||
cvc: '', | |||
number: '', | |||
expiry: '', | |||
zip: '', | |||
}, | |||
cardNumber: '', | |||
cardExpiry: '', | |||
cardCvc: '', | |||
cardZip: '', | |||
brand: '', | |||
stripe: null, | |||
stripeError: '', | |||
submitted: false, | |||
}; | |||
}, | |||
computed: { | |||
...mapState('system', ['messages']), | |||
...mapState('paymentMethods', ['paymentMethods', 'paymentStatus']), | |||
...mapState('plans', ['plans']), | |||
planObjects: function() { | |||
const plans_array = []; | |||
if (this.plans) { | |||
for (let i = 0; i < this.plans.length; i++) { | |||
plans_array.push({ | |||
...this.plans[i], | |||
localName: this.messages['plan_name_' + this.plans[i].name], | |||
description: this.messages[ | |||
'plan_description_' + this.plans[i].name | |||
], | |||
priceMajorUnits: this.plans[i].price / 100, | |||
priceMinorUnits: this.plans[i].price % 100, | |||
}); | |||
} | |||
this.bubblePlan = this.plans[0].name; | |||
} | |||
return plans_array; | |||
}, | |||
}, | |||
created() { | |||
this.initDefaults(); | |||
}, | |||
mounted() {}, | |||
methods: { | |||
...mapActions('paymentMethods', [ | |||
'getAllPaymentMethods', | |||
'setPaymentMethod', | |||
'addAccountPaymentMethod', | |||
'getAllAccountPaymentMethods', | |||
]), | |||
...mapActions('plans', ['getAllPlans']), | |||
setUpStripe() { | |||
if (window.Stripe === undefined) { | |||
alert('Stripe V3 library not loaded!'); | |||
} else { | |||
const stripe = window.Stripe( | |||
this.paymentMethods[0].driverConfig.publicApiKey | |||
); | |||
this.stripe = stripe; | |||
const style = { | |||
base: { | |||
padding: '.375rem .75rem', | |||
fontSize: '1rem', | |||
fontWeight: '400', | |||
lineHeight: '1.5', | |||
color: '#495057', | |||
}, | |||
}; | |||
const elements = stripe.elements(); | |||
this.cardCvc = elements.create('cardCvc', { style }); | |||
this.cardExpiry = elements.create('cardExpiry', { style }); | |||
this.cardNumber = elements.create('cardNumber', { style }); | |||
this.cardZip = elements.create('postalCode', { | |||
placeholder: 'Zip', | |||
style, | |||
}); | |||
this.cardNumber.update({ | |||
placeholder: 'CC Card #', | |||
showIcon: true, | |||
}); | |||
this.cardCvc.mount('#card-cvc'); | |||
this.cardExpiry.mount('#card-expiry'); | |||
this.cardNumber.mount('#card-number'); | |||
this.cardZip.mount('#card-zip'); | |||
this.listenForErrors(); | |||
} | |||
}, | |||
listenForErrors() { | |||
this.cardNumber.addEventListener('change', (event) => { | |||
this.toggleError(event); | |||
this.cardNumberError = ''; | |||
this.card.number = event.complete ? true : false; | |||
if (this.card.number) { | |||
this.brand = event.brand; | |||
} | |||
}); | |||
this.cardExpiry.addEventListener('change', (event) => { | |||
this.toggleError(event); | |||
this.cardExpiryError = ''; | |||
this.card.expiry = event.complete ? true : false; | |||
}); | |||
this.cardCvc.addEventListener('change', (event) => { | |||
this.toggleError(event); | |||
this.cardCvcError = ''; | |||
this.card.cvc = event.complete ? true : false; | |||
}); | |||
}, | |||
toggleError(event) { | |||
if (event.error) { | |||
this.stripeError = event.error.message; | |||
} else { | |||
this.stripeError = ''; | |||
} | |||
}, | |||
initDefaults() { | |||
this.getAllPlans(this.messages, this.errors); | |||
this.getAllPaymentMethods(this.messages, this.errors); | |||
}, | |||
authorizeCard(e) { | |||
util.setSkipRegistration(); | |||
this.errors.clear(); | |||
this.stripe.createToken(this.cardNumber).then((result) => { | |||
if (result.error) { | |||
this.$snotify.error(result.error.message); | |||
} else { | |||
this.submitted = true; | |||
this.addAccountPaymentMethod({ | |||
userId: this.user && this.user.uuid ? this.user.uuid : null, | |||
paymentMethod: { | |||
paymentMethodType: 'credit', | |||
paymentInfo: result.token.id, | |||
}, | |||
messages: this.messages, | |||
errors: this.errors, | |||
}); | |||
} | |||
}); | |||
return false; | |||
}, | |||
}, | |||
watch: { | |||
paymentMethods() { | |||
this.setUpStripe(); | |||
}, | |||
paymentStatus(ps) { | |||
if (ps && ps.addedPaymentMethod) { | |||
// refresh account payment methods | |||
this.getAllAccountPaymentMethods({ | |||
userId: this.user.uuid, | |||
messages: this.messages, | |||
errors: this.errors, | |||
}); | |||
} | |||
}, | |||
}, | |||
}; | |||
</script> |
@@ -0,0 +1,124 @@ | |||
<template> | |||
<div class="wrapper"> | |||
<h1 class="text-center white-text form-title"> | |||
{{ messages.verify_email_title }} | |||
</h1> | |||
<h4 class="d-flex align-items-center justify-content-center form-sub-title"> | |||
<span class="text-center white-text"> | |||
{{ messages.resend_verify_email_label }} | |||
</span> | |||
<a class="resend-btn" href="#" @click="resendVerification(firstContact)"> | |||
{{ messages.button_label_resend_verify_email }} | |||
</a> | |||
</h4> | |||
<div class="d-flex justify-content-center mt-5"> | |||
<div ref="lottie" class="lottie"></div> | |||
</div> | |||
<Features></Features> | |||
<a | |||
class="features-section-link text-center d-block" | |||
href="https://getbubblenow.com/features/" | |||
> | |||
{{ messages.more_features_label }} | |||
</a> | |||
</div> | |||
</template> | |||
<style lang="scss" scoped> | |||
@import '../../../_scss/components/form'; | |||
.features-section-link { | |||
color: $vivid-navy; | |||
font-size: 16px; | |||
margin-top: 25px; | |||
} | |||
.sub-title { | |||
font-size: 24px; | |||
} | |||
.lottie { | |||
width: 400px; | |||
} | |||
.resend-btn { | |||
text-decoration: underline; | |||
color: white; | |||
margin-left: 10px; | |||
} | |||
</style> | |||
<script> | |||
import { mapState, mapActions } from 'vuex'; | |||
import Lottie from 'lottie-web'; | |||
import { util } from '~/_helpers' | |||
import { Features } from '~/_components/sections'; | |||
// convenience methods | |||
import { isAuthenticator, isNotAuthenticator } from '~/_store/users.module'; | |||
window.isAuthenticator = isAuthenticator; | |||
window.isNotAuthenticator = isNotAuthenticator; | |||
export default { | |||
components: { | |||
Features, | |||
}, | |||
data() { | |||
return { | |||
firstContact: null, | |||
}; | |||
}, | |||
computed: { | |||
...mapState('system', ['messages']), | |||
...mapState('users', ['policy']), | |||
}, | |||
mounted() { | |||
Lottie.loadAnimation({ | |||
container: this.$refs.lottie, | |||
renderer: '', | |||
loop: true, | |||
autoplay: true, | |||
path: '/green_email_sent.json', | |||
}); | |||
}, | |||
methods: { | |||
...mapActions('account', ['resendVerificationCode']), | |||
getFirstContact(policy) { | |||
if (policy && policy.accountContacts) { | |||
const contacts = policy.accountContacts; | |||
for (let i = 0; i < contacts.length; i++) { | |||
if (isNotAuthenticator(contacts[i])) return contacts[i]; | |||
} | |||
return null; | |||
} | |||
return null; | |||
}, | |||
resendVerification(contact) { | |||
console.log(contact); | |||
this.resendVerificationCode({ | |||
userId: util.currentUser().uuid, | |||
contact: contact, | |||
messages: this.messages, | |||
errors: this.errors, | |||
}); | |||
return false; // do not follow the click | |||
}, | |||
}, | |||
watch: { | |||
policy(p) { | |||
this.firstContact = this.getFirstContact(p); | |||
}, | |||
}, | |||
}; | |||
</script> |
@@ -73,18 +73,22 @@ export const router = new Router({ | |||
children: [ | |||
{ | |||
path: '', | |||
component: () => import('~/_pages/auth/Layout'), | |||
component: () => import('~/_pages/main/Layout'), | |||
children: [ | |||
{ | |||
path: 'verify-email', | |||
component: () => import('~/_pages/auth/VerifyEmail'), | |||
path: '', | |||
component: () => import('~/_pages/main/account/Layout'), | |||
children: [ | |||
{ | |||
path: 'verify-email', | |||
component: () => import('~/_pages/main/account/VerifyEmail'), | |||
}, | |||
{ | |||
path: 'payment', | |||
component: () => import('~/_pages/main/account/Payment'), | |||
}, | |||
], | |||
}, | |||
], | |||
}, | |||
{ | |||
path: '', | |||
component: () => import('~/_pages/main/Layout'), | |||
children: [ | |||
{ | |||
path: 'test', | |||
component: () => import('~/_pages/main/Test'), | |||
@@ -14,7 +14,7 @@ $form-border-radius: 2px; | |||
margin-top: 16px; | |||
} | |||
.auth-form { | |||
.bubble-form { | |||
background-color: white; | |||
box-shadow: $form-box-shadow; | |||
@@ -34,7 +34,7 @@ $form-border-radius: 2px; | |||
} | |||
} | |||
.auth-form-submit { | |||
.bubble-form-submit { | |||
margin-top: 3rem; | |||
} | |||
@@ -106,3 +106,12 @@ String.prototype.parseDateMessage = function (millis, messages) { | |||
return evalInContext(context, expression) | |||
}) : ''; | |||
}; | |||
String.prototype.parsePrice = function () { | |||
const price = +this; | |||
if (price % 100 === 0) { | |||
return (price / 100).toString(); | |||
} else { | |||
return (price / 100).toFixed(2); | |||
} | |||
} |
@@ -8,7 +8,8 @@ import { router } from '~/_router'; | |||
import { account } from './account.module'; | |||
import staticMessages from '~/_assets/messages.json'; | |||
import preAuthStaticMessages from '~/_assets/pre_auth_messages.json'; | |||
import postAuthStaticMessages from '~/_assets/post_auth_messages.json'; | |||
const state = { | |||
configs: { | |||
@@ -68,7 +69,8 @@ const state = { | |||
} | |||
return { count: parseInt(ms), units: '' }; | |||
}, | |||
...staticMessages, | |||
...preAuthStaticMessages, | |||
...postAuthStaticMessages, | |||
}, | |||
messageGroupsLoaded: [], | |||
countries: [], | |||
@@ -337,9 +339,9 @@ const getters = { | |||
promoCodeRequired: function() { | |||
return state.promoCodePolicy === 'required'; | |||
}, | |||
configs: function () { | |||
configs: function() { | |||
return state.configs.loaded === true ? state.configs : {}; | |||
} | |||
}, | |||
}; | |||
const mutations = { | |||
@@ -368,7 +370,7 @@ const mutations = { | |||
loadSystemConfigsRequest(state) {}, | |||
loadSystemConfigsSuccess(state, configs) { | |||
state.configs = {...configs, loaded: true}; | |||
state.configs = { ...configs, loaded: true }; | |||
}, | |||
loadSystemConfigsFailure(state, error) { | |||
state.error = error; | |||