Ver a proveniência

feat: implement payment page

pull/28/head
Tyler Chen há 4 anos
ascendente
cometimento
8008bb527e
19 ficheiros alterados com 481 adições e 78 eliminações
  1. +0
    -6
      src/_assets/messages.json
  2. +7
    -0
      src/_assets/post_auth_messages.json
  3. +29
    -0
      src/_assets/pre_auth_messages.json
  4. +0
    -2
      src/_components/layout/Header.vue
  5. +1
    -1
      src/_components/sections/Features.vue
  6. +2
    -0
      src/_components/shared/Button.vue
  7. +13
    -3
      src/_components/shared/Card.vue
  8. +2
    -2
      src/_pages/auth/ForgotPassword.vue
  9. +9
    -3
      src/_pages/auth/Layout.vue
  10. +3
    -2
      src/_pages/auth/Login.vue
  11. +1
    -38
      src/_pages/auth/Register.vue
  12. +1
    -4
      src/_pages/main/Layout.vue
  13. +56
    -0
      src/_pages/main/account/Layout.vue
  14. +324
    -0
      src/_pages/main/account/Payment.vue
  15. +1
    -1
      src/_pages/main/account/VerifyEmail.vue
  16. +13
    -9
      src/_router/index.js
  17. +2
    -2
      src/_scss/components/_form.scss
  18. +9
    -0
      src/_store/index.js
  19. +8
    -5
      src/_store/system.module.js

+ 0
- 6
src/_assets/messages.json Ver ficheiro

@@ -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"
}

+ 7
- 0
src/_assets/post_auth_messages.json Ver ficheiro

@@ -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!"
}

+ 29
- 0
src/_assets/pre_auth_messages.json Ver ficheiro

@@ -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"
}

+ 0
- 2
src/_components/layout/Header.vue Ver ficheiro

@@ -145,11 +145,9 @@ export default {

methods: {
toggleNavbar() {
console.log('toggleNavbar');
this.menuVisible = !this.prevVisibleState;
},
hide() {
console.log('hide');
this.prevVisibleState = this.menuVisible;
this.menuVisible = false;
},


+ 1
- 1
src/_components/sections/Features.vue Ver ficheiro

@@ -10,7 +10,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"


+ 2
- 0
src/_components/shared/Button.vue Ver ficheiro

@@ -48,10 +48,12 @@

&.block {
display: block;
width: 100%;
}

.btn--text {
text-transform: uppercase;
white-space: nowrap;
padding: 5px 20px;
}



+ 13
- 3
src/_components/shared/Card.vue Ver ficheiro

@@ -1,5 +1,5 @@
<template>
<div class="card-container h-100">
<div class="card-container" :class="{ 'rounded-corner': roundCorner }">
<slot></slot>
</div>
</template>
@@ -8,8 +8,11 @@
.card-container {
background-color: white;
padding: 40px;
border-radius: 10px;
box-shadow: 0px 10px 50px #dee1ec;

&.rounded-corner {
border-radius: 10px;
}
}

.card-content {
@@ -46,5 +49,12 @@
</style>

<script>
export default {};
export default {
props: {
roundCorner: {
type: Boolean,
default: true,
},
},
};
</script>

+ 2
- 2
src/_pages/auth/ForgotPassword.vue Ver ficheiro

@@ -7,7 +7,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"
@@ -29,7 +29,7 @@
</div>
<Button
color="default"
class="auth-form-submit"
class="bubble-form-submit"
@click="handleSubmit"
:disabled="status.sendingResetPasswordMessage"
>


+ 9
- 3
src/_pages/auth/Layout.vue Ver ficheiro

@@ -41,13 +41,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';
},
},


+ 3
- 2
src/_pages/auth/Login.vue Ver ficheiro

@@ -9,7 +9,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"
@@ -130,8 +130,9 @@
{{ messages.button_label_forgotPassword }}
</router-link>
<Button
block
color="default"
class="auth-form-submit"
class="bubble-form-submit"
@click="submit"
:disabled="status.loggingIn"
>


+ 1
- 38
src/_pages/auth/Register.vue Ver ficheiro

@@ -7,7 +7,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"
@@ -156,43 +156,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
- 4
src/_pages/main/Layout.vue Ver ficheiro

@@ -1,8 +1,5 @@
<template>
<div>
Main Layout
<router-view></router-view>
</div>
<router-view></router-view>
</template>

<script>


+ 56
- 0
src/_pages/main/account/Layout.vue Ver ficheiro

@@ -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>

+ 324
- 0
src/_pages/main/account/Payment.vue Ver ficheiro

@@ -0,0 +1,324 @@
<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="submit">
<select class="form-control" v-model="bubblePlan">
<option
v-for="(plan, index) in messages.marketing_pricing_options.split(',')"
:key="index"
:value="plan"
>
{{
messages.label_pricing_option_format.parseExpression({
plan,
messages: messages,
price: messages[`marketing_pricing_${plan}_price`].parsePrice(),
})
}}
</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>

<Button block color="default" class="bubble-form-submit">
{{ 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 { Button, Card } from '~/_components/shared';

export default {
components: {
Button,
Card,
},

data: () => ({
bubblePlan: '',
card: {
cvc: '',
number: '',
expiry: '',
zip: '',
},
cardNumber: '',
cardExpiry: '',
cardCvc: '',
cardZip: '',
stripe: null,
stripeError: '',
}),

computed: {
...mapState('system', ['messages']),
...mapState('paymentMethods', ['paymentMethods']),
},

created() {
this.initDefaults();
},

mounted() {},

methods: {
...mapActions('paymentMethods', [
'getAllPaymentMethods',
'getAllAccountPaymentMethods',
'setPaymentMethod',
'getPromosByAccount',
]),

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.getAllPaymentMethods(this.messages, this.errors);
},

submit() {},
},

watch: {
paymentMethods() {
this.bubblePlan = this.messages.marketing_pricing_options.split(',')[0];
this.setUpStripe();
},
},
};
</script>

src/_pages/auth/VerifyEmail.vue → src/_pages/main/account/VerifyEmail.vue Ver ficheiro

@@ -28,7 +28,7 @@
</template>

<style lang="scss" scoped>
@import '../../_scss/components/form';
@import '../../../_scss/components/form';

.features-section-link {
color: $vivid-navy;

+ 13
- 9
src/_router/index.js Ver ficheiro

@@ -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'),


+ 2
- 2
src/_scss/components/_form.scss Ver ficheiro

@@ -13,7 +13,7 @@ $form-border-radius: 2px;
margin-top: 16px;
}

.auth-form {
.bubble-form {
background-color: white;
box-shadow: $form-box-shadow;

@@ -33,7 +33,7 @@ $form-border-radius: 2px;
}
}

.auth-form-submit {
.bubble-form-submit {
margin-top: 3rem;
}



+ 9
- 0
src/_store/index.js Ver ficheiro

@@ -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
- 5
src/_store/system.module.js Ver ficheiro

@@ -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;
@@ -423,6 +425,7 @@ const mutations = {
if (state.messageGroupsLoaded.indexOf(group) === -1)
state.messageGroupsLoaded.push(group);
state.messages = util.addMessages(state.messages, messages);
state.messages.marketing_pricing_options = 'personal,power,mega';
if (messages.country_codes) {
const countries = [];
const codes = messages.country_codes.split(',');


Carregando…
Cancelar
Guardar