New client settings (#8886)

* Added initial settings page

* Initial cleanup and translations

* Ported api settings

* Ported promocode settings

* POrted notifications code

* Fixed styles and translatins for site page

* Ported over rest of settings functions

* Ported payments over

* Initial lint clean up

* Added amazon modal

* Added stripe

* Added site settings
This commit is contained in:
Keith Holliday
2017-07-20 12:01:00 -06:00
committed by GitHub
parent 605391e4e7
commit 88f872ed50
16 changed files with 1565 additions and 2 deletions

View File

@@ -38,6 +38,18 @@ module.exports = {
target: 'http://localhost:3000', target: 'http://localhost:3000',
changeOrigin: true, changeOrigin: true,
}, },
'/stripe': {
target: 'http://localhost:3000',
changeOrigin: true,
},
'/amazon': {
target: 'http://localhost:3000',
changeOrigin: true,
},
'/paypal': {
target: 'http://localhost:3000',
changeOrigin: true,
},
}, },
// CSS Sourcemaps off by default because relative paths are "buggy" // CSS Sourcemaps off by default because relative paths are "buggy"
// with this option, according to the CSS-Loader README // with this option, according to the CSS-Loader README

View File

@@ -51,7 +51,7 @@ div
router-link.dropdown-item(:to="{name: 'stats'}") {{ $t('stats') }} router-link.dropdown-item(:to="{name: 'stats'}") {{ $t('stats') }}
router-link.dropdown-item(:to="{name: 'achievements'}") {{ $t('achievements') }} router-link.dropdown-item(:to="{name: 'achievements'}") {{ $t('achievements') }}
router-link.dropdown-item(:to="{name: 'profile'}") {{ $t('profile') }} router-link.dropdown-item(:to="{name: 'profile'}") {{ $t('profile') }}
router-link.dropdown-item(:to="{name: 'settings'}") {{ $t('settings') }} router-link.dropdown-item(:to="{name: 'site'}") {{ $t('settings') }}
a.nav-link.dropdown-item(to="/", @click.prevent='logout()') {{ $t('logout') }} a.nav-link.dropdown-item(to="/", @click.prevent='logout()') {{ $t('logout') }}
</template> </template>

View File

@@ -0,0 +1,201 @@
<template lang="pug">
b-modal#amazon-payment(title="Amazon", :hide-footer="true", size='lg')
button#AmazonPayButton
</template>
<script>
import axios from 'axios';
import bModal from 'bootstrap-vue/lib/components/modal';
const AMAZON_PAYMENTS = {
CLIENT_ID: 'testing',
SELLER_ID: 'test-seelllide',
};
export default {
components: {
bModal,
},
props: ['amazonPayments'],
data () {
return {
OffAmazonPayments: {},
isAmazonReady: false,
};
},
mounted () {
this.OffAmazonPayments = window.OffAmazonPayments;
this.isAmazonReady = true;
window.amazon.Login.setClientId(AMAZON_PAYMENTS.CLIENT_ID);
// @TODO: prevent modal close form clicking outside
this.OffAmazonPayments.Button('AmazonPayButton', AMAZON_PAYMENTS.SELLER_ID, { // eslint-disable-line
type: 'PwA',
color: 'Gold',
size: 'small',
agreementType: 'BillingAgreement',
onSignIn: async (contract) => {
this.amazonPaymentsbillingAgreementId = contract.getAmazonBillingAgreementId();
if (this.amazonPaymentstype === 'subscription') {
this.amazonPaymentsloggedIn = true;
this.amazonPaymentsinitWidgets();
} else {
let url = '/amazon/createOrderReferenceId';
let response = await axios.post(url, {
billingAgreementId: this.amazonPaymentsbillingAgreementId,
});
// @TODO: Success
this.amazonPaymentsloggedIn = true;
this.amazonPaymentsorderReferenceId = response.data.orderReferenceId;
this.amazonPaymentsinitWidgets();
// @TODO: error
alert(response.message);
}
},
authorization: () => {
window.amazon.Login.authorize({
scope: 'payments:widget',
popup: true,
}, function amazonSuccess (response) {
if (response.error) return alert(response.error);
let url = '/amazon/verifyAccessToken';
axios.post(url, response)
.catch((e) => {
alert(e.message);
});
});
},
onError: this.amazonOnError,
});
},
methods: {
amazonPaymentsCanCheckout () {
// if (this.amazonPaymentstype === 'single') {
// return this.amazonPaymentspaymentSelected === true;
// } else if(this.amazonPaymentstype === 'subscription') {
// return this.amazonPaymentspaymentSelected === true &&
// // Mah.. one is a boolean the other a string...
// this.amazonPaymentsrecurringConsent === 'true';
// } else {
// return false;
// }
},
amazonInitWidgets () {
let walletParams = {
sellerId: AMAZON_PAYMENTS.SELLER_ID, // @TODO: Import
design: {
designMode: 'responsive',
},
onPaymentSelect: () => {
this.amazonPaymentspaymentSelected = true;
},
onError: this.amazonOnError,
};
if (this.amazonPaymentstype === 'subscription') {
walletParams.agreementType = 'BillingAgreement';
walletParams.billingAgreementId = this.amazonPaymentsbillingAgreementId;
walletParams.onReady = (billingAgreement) => {
this.amazonPaymentsbillingAgreementId = billingAgreement.getAmazonBillingAgreementId();
new this.OffAmazonPayments.Widgets.Consent({
sellerId: AMAZON_PAYMENTS.SELLER_ID,
amazonBillingAgreementId: this.amazonPaymentsbillingAgreementId,
design: {
designMode: 'responsive',
},
onReady: (consent) => {
let getConsent = consent.getConsentStatus;
this.amazonPaymentsrecurringConsent = getConsent ? getConsent() : false;
},
onConsent: (consent) => {
this.amazonPaymentsrecurringConsent = consent.getConsentStatus();
},
onError: this.amazonOnError,
}).bind('AmazonPayRecurring');
};
} else {
walletParams.amazonOrderReferenceId = this.amazonPaymentsorderReferenceId;
}
new this.OffAmazonPayments.Widgets.Wallet(walletParams).bind('AmazonPayWallet');
},
async amazonCheckOut () {
this.amazonButtonEnabled = false;
if (this.amazonPaymentstype === 'single') {
let url = '/amazon/checkout';
let response = await axios.post(url, {
orderReferenceId: this.amazonPaymentsorderReferenceId,
gift: this.amazonPaymentsgift,
});
// Success
this.amazonPaymentsreset();
window.location.reload(true);
// Failure
alert(response.message);
this.amazonPaymentsreset();
} else if (this.amazonPaymentstype === 'subscription') {
let url = '/amazon/subscribe';
if (this.amazonPaymentsgroupToCreate) {
url = '/api/v3/groups/create-plan';
}
let response = await axios.post(url, {
billingAgreementId: this.amazonPaymentsbillingAgreementId,
subscription: this.amazonPaymentssubscription,
coupon: this.amazonPaymentscoupon,
groupId: this.amazonPaymentsgroupId,
groupToCreate: this.amazonPaymentsgroupToCreate,
paymentType: 'Amazon',
});
// IF success
this.amazonPaymentsreset();
if (response && response.data && response.data._id) {
this.$router.push(`/groups/guilds/${response.data._id}`);
} else {
this.$router.push('/');
}
// if fails
alert(response.message);
this.amazonPaymentsreset();
}
},
amazonOnError (error) {
alert(error.getErrorMessage());
// @TODO: this.amazonPaymentsreset();
},
reset () {
this.amazonPaymentsmodal.close(); // @TODO: this.$root.$emit('hide::modal', 'guild-form');
this.amazonPaymentsmodal = null;
this.amazonPaymentstype = null;
this.amazonPaymentsloggedIn = false;
this.amazonPaymentsgift = null;
this.amazonPaymentsbillingAgreementId = null;
this.amazonPaymentsorderReferenceId = null;
this.amazonPaymentspaymentSelected = false;
this.amazonPaymentsrecurringConsent = false;
this.amazonPaymentssubscription = null;
this.amazonPaymentscoupon = null;
},
},
};
</script>

View File

@@ -0,0 +1,118 @@
<template lang="pug">
.row.standard-page
.col-6
h2 {{ $t('API') }}
small {{ $t('APIText') }}
.section
h6 {{ $t('userId') }}
pre.prettyprint {{user.id}}
h6 {{ $t('APIToken') }}
pre.prettyprint {{user.apiToken}}
small(v-html='$t("APITokenWarning", { hrefTechAssistanceEmail })')
.section
h3 {{ $t('thirdPartyApps') }}
ul
li
a(target='_blank' href='https://www.beeminder.com/habitica') {{ $t('beeminder') }}
br
| {{ $t('beeminderDesc') }}
li
a(target='_blank' href='https://chrome.google.com/webstore/detail/habitrpg-chat-client/hidkdfgonpoaiannijofifhjidbnilbb') {{ $t('chromeChatExtension') }}
br
| {{ $t('chromeChatExtensionDesc') }}
li
a(target='_blank' :href='`http://data.habitrpg.com?uuid= + user._id`') {{ $t('dataTool') }}
br
| {{ $t('dataToolDesc') }}
li(v-html="$t('otherExtensions')")
br
| {{ $t('otherDesc') }}
hr
.col-6
h2 {{ $t('webhooks') }}
table.table.table-striped
thead(v-if='user.webhooks.length')
tr
th {{ $t('enabled') }}
th {{ $t('webhookURL') }}
th
tbody
tr(v-for="(webhook, index) in user.webhooks")
td
input(type='checkbox', v-model='webhook.enabled', @change='saveWebhook(webhook, index)')
td
input.form-control(type='url', v-model='webhook.url')
td
a.btn.btn-warning.checklist-icons(@click='deleteWebhook(webhook, index)')
span.glyphicon.glyphicon-trash(:tooltip="$t('delete')") Delete
a.btn.btn-success.checklist-icons(@click='saveWebhook(webhook, index)') Update
tr
td(colspan=2)
.form-horizontal
.form-group.col-sm-10
input.form-control(type='url', v-model='newWebhook.url', :placeholder="$t('webhookURL')")
.col-sm-2
button.btn.btn-sm.btn-primary(type='submit', @click='addWebhook(newWebhook.url)') {{ $t('add') }}
</template>
<style scope>
.section {
margin-top: 2em;
}
</style>
<script>
import { mapState } from 'client/libs/store';
import uuid from '../../../common/script/libs/uuid';
// @TODO: env.EMAILS.TECH_ASSISTANCE_EMAIL
const TECH_ASSISTANCE_EMAIL = 'admin@habitica.com';
export default {
data () {
return {
newWebhook: {
url: '',
},
hrefTechAssistanceEmail: `<a href="mailto:${TECH_ASSISTANCE_EMAIL}">${TECH_ASSISTANCE_EMAIL}</a>`,
};
},
computed: {
...mapState({user: 'user.data'}),
},
methods: {
async addWebhook (url) {
let webhookInfo = {
id: uuid(),
type: 'taskActivity',
options: {
created: false,
updated: false,
deleted: false,
scored: true,
},
url,
enabled: true,
};
let webhook = await this.$store.dispatch('user:addWebhook', {webhookInfo});
this.user.webhooks.push(webhook);
this.newWebhook.url = '';
},
async saveWebhook (webhook, index) {
delete webhook._editing;
let updatedWebhook = await this.$store.dispatch('user:updateWebhook', {webhook});
this.user.webhooks[index] = updatedWebhook;
},
async deleteWebhook (webhook, index) {
delete webhook._editing;
await this.$store.dispatch('user:deleteWebhook', {webhook});
this.user.webhooks.splice(index, 1);
},
},
};
</script>

View File

@@ -0,0 +1,13 @@
<template lang="pug">
.row
.col-md-6
h2 {{ $t('dataExport') }}
small {{ $t('saveData') }}
h4 {{ $t('habitHistory') }}
| {{ $t('exportHistory') }}
a(href="/export/history.csv") {{ $t('csv') }}
h4 {{ $t('userData') }}
| {{ $t('exportUserData') }}
a(href="/export/userdata.xml") {{ $t('xml') }}
a(href="/export/userdata.json") {{ $t('json') }}
</template>

View File

@@ -0,0 +1,52 @@
<template lang="pug">
b-modal#delete(:title="$t('deleteAccount')", :hide-footer='true' size='md')
strong {{ $t('deleteLocalAccountText') }}
br
.row
.col-6
input.form-control(type='password', v-model='password')
br
.row
#feedback.col-12.form-group
label(for='feedbackTextArea') {{ $t('feedback') }}
textarea#feedbackTextArea.form-control(v-model='feedback')
.modal-footer
button.btn.btn-danger(@click='close()') {{ $t('neverMind') }}
button.btn.btn-primary(@click='deleteAccount()', :disabled='!password') {{ $t('deleteDo') }}
</template>
<script>
import axios from 'axios';
import { mapState } from 'client/libs/store';
import bModal from 'bootstrap-vue/lib/components/modal';
export default {
components: {
bModal,
},
data () {
return {
password: '',
feedback: '',
};
},
computed: {
...mapState({user: 'user.data'}),
},
methods: {
close () {
this.$root.$emit('hide::modal', 'reset');
},
async deleteAccount () {
await axios.delete('/api/v3/user/', {
password: this.password,
feedback: this.feedback,
});
localStorage.clear();
this.$router.push('/');
this.$root.$emit('hide::modal', 'reset');
},
},
};
</script>

View File

@@ -0,0 +1,23 @@
<template lang="pug">
.row
secondary-menu.col-12
router-link.nav-link(:to="{name: 'site'}", exact, :class="{'active': $route.name === 'site'}") {{ $t('site') }}
router-link.nav-link(:to="{name: 'api'}", :class="{'active': $route.name === 'api'}") {{ $t('API') }}
router-link.nav-link(:to="{name: 'dataExport'}", :class="{'active': $route.name === 'dataExport'}") {{ $t('dataExport') }}
router-link.nav-link(:to="{name: 'promoCode'}", :class="{'active': $route.name === 'promoCode'}") {{ $t('promoCode') }}
router-link.nav-link(:to="{name: 'subscription'}", :class="{'active': $route.name === 'subscription'}") {{ $t('subscription') }}
router-link.nav-link(:to="{name: 'notifications'}", :class="{'active': $route.name === 'notifications'}") {{ $t('notifications') }}
.col-12
router-view
</template>
<script>
import SecondaryMenu from 'client/components/secondaryMenu';
export default {
components: {
SecondaryMenu,
},
};
</script>

View File

@@ -0,0 +1,74 @@
<template lang="pug">
.row.standard-page
.col-12
h1 {{ $t('notifications') }}
.col-12
.checkbox
label
input(type='checkbox', v-model='user.preferences.pushNotifications.unsubscribeFromAll',
@change='set("pushNotifications", "unsubscribeFromAll")')
span {{ $t('unsubscribeAllPush') }}
br
.checkbox
label
input(type='checkbox', v-model='user.preferences.emailNotifications.unsubscribeFromAll',
@change='set("emailNotifications", "unsubscribeFromAll")')
span {{ $t('unsubscribeAllEmails') }}
small {{ $t('unsubscribeAllEmailsText') }}
.col-8
table.table
tr
td
th
span {{ $t('email') }}
th
span {{ $t('push') }}
tr(v-for='notification in notifications')
td
span {{ $t(notification) }}
td
input(type='checkbox', v-model='user.preferences.emailNotifications[notification]',
@change='set("emailNotifications", notification)')
td
input(type='checkbox', v-model='user.preferences.pushNotifications[notification]',
@change='set("pushNotifications", notification)')
hr
</template>
<script>
import { mapState } from 'client/libs/store';
export default {
data () {
return {
notifications: [
'newPM',
'wonChallenge',
'giftedGems',
'giftedSubscription',
'invitedParty',
'invitedGuild',
'kickedGroup',
'questStarted',
'invitedQuest',
'importantAnnouncements',
'weeklyRecaps',
'onboarding',
],
};
},
computed: {
...mapState({user: 'user.data'}),
},
methods: {
set (preferenceType, notification) {
let settings = {};
settings[`preferences.${preferenceType}.${notification}`] = this.user.preferences[preferenceType][notification];
this.$store.dispatch('user:set', settings);
},
},
};
</script>

View File

@@ -0,0 +1,61 @@
<template lang="pug">
.row.standard-page
.col-md-6
h2 {{ $t('promoCode') }}
.form-inline(role='form')
input.form-control(type='text', v-model='couponCode', :placeholder="$t('promoPlaceholder')")
button.btn.btn-primary(@click='enterCoupon()') {{ $t('submit') }}
div
small {{ $t('couponText') }}
div(v-if='user.contributor.sudo')
hr
h4 {{ $t('generateCodes') }}
.form(role='form')
.form-group
input.form-control(type='text', v-model='codes.event', placeholder="Event code (eg, 'wondercon')")
.form-group
input.form-control(type='number', v-model='codes.count', placeholder="Number of codes to generate (eg, 250)")
.form-group
button.btn.btn-primary(type='submit', @click='generateCodes(codes)') {{ $t('generate') }}
a.btn.btn-default(:href='getCodesUrl') {{ $t('getCodes') }}
</template>
<script>
import axios from 'axios';
import { mapState } from 'client/libs/store';
export default {
data () {
return {
codes: {
event: '',
count: '',
},
couponCode: '',
};
},
computed: {
...mapState({user: 'user.data'}),
getCodesUrl () {
if (!this.user) return '';
return `/api/v3/coupons?_id=${this.user._id}&apiToken=${this.user.apiToken}`;
},
},
methods: {
generateCodes () {
// $http.post(ApiUrl.get() + '/api/v2/coupons/generate/'+codes.event+'?count='+(codes.count || 1))
// .success(function(res,code){
// $scope._codes = {};
// if (code!==200) return;
// window.location.href = '/api/v2/coupons?limit='+codes.count+'&_id='+User.user._id+'&apiToken='+User.settings.auth.apiToken;
// })
},
async enterCoupon () {
let code = await axios.get(`/api/v3/coupons/enter/${this.couponCode}`);
if (!code) return;
// @TODO: what needs to be updated? User.sync();
// @TODO: mixin Notification.text(env.t('promoCodeApplied'));
},
},
};
</script>

View File

@@ -0,0 +1,36 @@
<template lang="pug">
b-modal#reset(:title="$t('resetAccount')", :hide-footer='true' size='md')
p {{ $t('resetText1') }}
p {{ $t('resetText2') }}
.modal-footer
button.btn.btn-danger(@click='close()') {{ $t('neverMind') }}
button.btn.btn-primary(@click='reset()') {{ $t('resetDo') }}
</template>
<script>
import axios from 'axios';
import { mapState } from 'client/libs/store';
import bModal from 'bootstrap-vue/lib/components/modal';
export default {
components: {
bModal,
},
computed: {
...mapState({user: 'user.data'}),
},
methods: {
close () {
this.$root.$emit('hide::modal', 'reset');
},
async reset () {
let response = await axios.post('/api/v3/user/reset');
// @TODO: Not sure if this is correct
this.$store.user = response.data.data.user;
this.$router.push('/');
this.$root.$emit('hide::modal', 'reset');
},
},
};
</script>

View File

@@ -0,0 +1,109 @@
<template lang="pug">
b-modal#restore(:title="$t('fixValues')", :hide-footer='true' size='lg')
p {{ $t('fixValuesText1') }}
p {{ $t('fixValuesText2') }}
.form-horizontal
h3 {{ $t('stats') }}
.form-group.row
.col-sm-3
label.control-label {{ $t('health') }}
.col-sm-9
input.form-control(type='number', step="any", data-for='stats.hp', v-model='restoreValues.stats.hp')
.form-group.row
.col-sm-3
label.control-label {{ $t('experience') }}
.col-sm-9
input.form-control(type='number', step="any", data-for='stats.exp', v-model='restoreValues.stats.exp')
.form-group.row
.col-sm-3
label.control-label {{ $t('gold') }}
.col-sm-9
input.form-control(type='number', step="any", data-for='stats.gp', v-model='restoreValues.stats.gp')
//input.form-control(type='number', step="any", data-for='stats.gp', v-model='restoreValues.stats.gp',disabled)
//-p.alert
small {{ $t('disabledWinterEvent') }}
.form-group.row
.col-sm-3
label.control-label {{ $t('mana') }}
.col-sm-9
input.form-control(type='number', step="any", data-for='stats.mp', v-model='restoreValues.stats.mp')
.form-group.row
.col-sm-3
label.control-label {{ $t('level') }}
.col-sm-9
input.form-control(type='number', data-for='stats.lvl', v-model='restoreValues.stats.lvl')
h3 {{ $t('achievements') }}
.form-group.row
.col-sm-3
label.control-label {{ $t('fix21Streaks') }}
.col-sm-9
input.form-control(type='number', data-for='achievements.streak', v-model='restoreValues.achievements.streak')
//- This is causing too many problems for users
h3 {{ $t('other') }}
a.btn.btn-sm.btn-warning(ng-controller='FooterCtrl', ng-click='addMissedDay(1)') {{ $t('triggerDay') }}
.modal-footer
button.btn.btn-danger(@click='close()') {{ $t('discardChanges') }}
button.btn.btn-primary(@click='restore()') {{ $t('saveAndClose') }}
</template>
<script>
import { mapState } from 'client/libs/store';
import bModal from 'bootstrap-vue/lib/components/modal';
export default {
components: {
bModal,
},
data () {
return {
restoreValues: {
stats: {
hp: 0,
mp: 0,
gp: 0,
exp: 0,
lvl: 0,
},
achievements: {
streak: 0,
},
},
};
},
mounted () {
this.restoreValues.stats = this.user.stats;
this.restoreValues.achievements.streak = this.user.achievements.streak;
},
computed: {
...mapState({user: 'user.data'}),
},
methods: {
close () {
this.$root.$emit('hide::modal', 'restore');
},
restore () {
if (this.restoreValues.stats.lvl < 1) {
// @TODO:
// Notification.error(env.t('invalidLevel'), true);
return;
}
this.user.stats = this.restoreValues.stats;
this.user.achievements.streak = this.restoreValues.achievements.streak;
let settings = {
'stats.hp': this.restoreValues.stats.hp,
'stats.exp': this.restoreValues.stats.exp,
'stats.gp': this.restoreValues.stats.gp,
'stats.lvl': this.restoreValues.stats.lvl,
'stats.mp': this.restoreValues.stats.mp,
'achievements.streak': this.restoreValues.achievements.streak,
};
this.$store.dispatch('user:set', settings);
this.$root.$emit('hide::modal', 'restore');
},
},
};
</script>

View File

@@ -0,0 +1,371 @@
<template lang="pug">
.row.standard-page
restore-modal
reset-modal
delete-modal
h1.col-12 {{ $t('settings') }}
.col-6
.form-horizontal
h5 {{ $t('language') }}
select.form-control(v-model='selectedLanguage',
@change='changeLanguage()')
option(v-for='lang in availableLanguages', :value='lang.code') {{lang.name}}
small
| {{ $t('americanEnglishGovern') }}
br
strong(v-html="$t('helpWithTranslation')")
hr
.form-horizontal
h5 {{ $t('dateFormat') }}
select.form-control(v-model='user.preferences.dateFormat',
@change='set("dateFormat")')
option(v-for='dateFormat in availableFormats', :value='dateFormat') {{dateFormat}}
hr
div
.checkbox
label
input(type='checkbox', @click='hideHeader() ', v-model='user.preferences.hideHeader')
span.hint(popover-trigger='mouseenter', popover-placement='right', :popover="$t('showHeaderPop')") {{ $t('showHeader') }}
.checkbox
label
input(type='checkbox', @click='toggleStickyHeader()', v-model='user.preferences.stickyHeader', :disabled="user.preferences.hideHeader")
span.hint(popover-trigger='mouseenter', popover-placement='right', :popover="$t('stickyHeaderPop')") {{ $t('stickyHeader') }}
.checkbox
label
input(type='checkbox', v-model='user.preferences.newTaskEdit', @click='set("newTaskEdit")')
span.hint(popover-trigger='mouseenter', popover-placement='right', :popover="$t('newTaskEditPop')") {{ $t('newTaskEdit') }}
.checkbox
label
input(type='checkbox', v-model='user.preferences.tagsCollapsed', @change='set("tagsCollapsed")')
span.hint(popover-trigger='mouseenter', popover-placement='right', :popover="$t('startCollapsedPop')") {{ $t('startCollapsed') }}
.checkbox
label
input(type='checkbox', v-model='user.preferences.advancedCollapsed', @change='set("advancedCollapsed")')
span.hint(popover-trigger='mouseenter', popover-placement='right', :popover="$t('startAdvCollapsedPop')") {{ $t('startAdvCollapsed') }}
.checkbox
label
input(type='checkbox', v-model='user.preferences.dailyDueDefaultView', @change='set("dailyDueDefaultView")')
span.hint(popover-trigger='mouseenter', popover-placement='right', :popover="$t('dailyDueDefaultViewPop')") {{ $t('dailyDueDefaultView') }}
.checkbox(v-if='party.memberCount === 1')
label
input(type='checkbox', v-model='user.preferences.displayInviteToPartyWhenPartyIs1', @change='set("displayInviteToPartyWhenPartyIs1")')
span.hint(popover-trigger='mouseenter', popover-placement='right', :popover="$t('displayInviteToPartyWhenPartyIs1')") {{ $t('displayInviteToPartyWhenPartyIs1') }}
.checkbox
input(type='checkbox', v-model='user.preferences.suppressModals.levelUp', @change='set("suppressModals", "levelUp")')
label {{ $t('suppressLevelUpModal') }}
.checkbox
input(type='checkbox', v-model='user.preferences.suppressModals.hatchPet', @change='set("suppressModals", "hatchPet")')
label {{ $t('suppressHatchPetModal') }}
.checkbox
input(type='checkbox', v-model='user.preferences.suppressModals.raisePet', @change='set("suppressModals", "raisePet")')
label {{ $t('suppressRaisePetModal') }}
.checkbox
input(type='checkbox', v-model='user.preferences.suppressModals.streak', @change='set("suppressModals", "streak")')
label {{ $t('suppressStreakModal') }}
//- .checkbox
//- label {{ $t('confirmScoreNotes') }}
//- input(type='checkbox', v-model='user.preferences.tasks.confirmScoreNotes', @change='set({"preferences.tasks.confirmScoreNotes": user.preferences.tasks.confirmScoreNotes ? true: false})')
//- .checkbox
//- label {{ $t('groupTasksByChallenge') }}
//- input(type='checkbox', v-model='user.preferences.tasks.groupByChallenge', @change='set({"preferences.tasks.groupByChallenge": user.preferences.tasks.groupByChallenge ? true: false})')
hr
button.btn.btn-primary(@click='showBailey()', popover-trigger='mouseenter', popover-placement='right', :popover="$t('showBaileyPop')") {{ $t('showBailey') }}
button.btn.btn-primary(@click='openRestoreModal()', popover-trigger='mouseenter', popover-placement='right', :popover="$t('fixValPop')") {{ $t('fixVal') }}
button.btn.btn-primary(v-if='user.preferences.disableClasses == true', @click='changeClass({})',
popover-trigger='mouseenter', popover-placement='right', :popover="$t('enableClassPop')") {{ $t('enableClass') }}
hr
div
h5 {{ $t('customDayStart') }}
.alert.alert-warning {{ $t('customDayStartInfo1') }}
.form-horizontal
.form-group
.col-7
select.form-control(v-model='newDayStart')
option(v-for='option in dayStartOptions' :value='option.value') {{option.name}}
.col-5
button.btn.btn-block.btn-primary(@click='openDayStartModal()',
:disabled='newDayStart === user.preferences.dayStart')
| {{ $t('saveCustomDayStart') }}
hr
h5 {{ $t('timezone') }}
.form-horizontal
.form-group
.col-12
p(v-html="$t('timezoneUTC', {utc: timezoneOffsetToUtc})")
p(v-html="$t('timezoneInfo')")
.col-6
h2 {{ $t('registration') }}
.panel-body
div
ul.list-inline
li(v-for='network in SOCIAL_AUTH_NETWORKS')
button.btn.btn-primary(v-if='!user.auth[network.key].id', @click='socialLogin(network.key, user)') {{ $t('registerWithSocial', {network: network.name}) }}
button.btn.btn-primary(disabled='disabled', v-if='!hasBackupAuthOption(network.key) && user.auth[network.key].id') {{ $t('registeredWithSocial', {network: network.name}) }}
button.btn.btn-danger(@click='deleteSocialAuth(network.key)', v-if='hasBackupAuthOption(network.key) && user.auth[network.key].id') {{ $t('detachSocial', {network: network.name}) }}
hr
div(v-if='!user.auth.local.username')
p {{ $t('addLocalAuth') }}
form(ng-submit='http("post", "/api/v3/user/auth/local/register", localAuth, "addedLocalAuth")', name='localAuth', novalidate)
//-.alert.alert-danger(ng-messages='changeUsername.$error && changeUsername.submitted') {{ $t('fillAll') }}
.form-group
input.form-control(type='text', placeholder="$t('username')", v-model='localAuth.username', required)
.form-group
input.form-control(type='text', placeholder="$t('email')", v-model='localAuth.email', required)
.form-group
input.form-control(type='password', placeholder="$t('password')", v-model='localAuth.password', required)
.form-group
input.form-control(type='password', placeholder="$t('confirmPass')", v-model='localAuth.confirmPassword', required)
button.btn.btn-primary(type='submit', ng-disabled='localAuth.$invalid', value="$t('submit')")
.usersettings(v-if='user.auth.local.username')
p {{ $t('username') }}
|: {{user.auth.local.username}}
p
small.muted
| {{ $t('loginNameDescription1') }}
|&nbsp;
a(href='/#/options/profile/profile') {{ $t('loginNameDescription2') }}
|&nbsp;
| {{ $t('loginNameDescription3') }}
p {{ $t('email') }}
|: {{user.auth.local.email}}
hr
h5 {{ $t('changeUsername') }}
.form(v-if='user.auth.local', name='changeUsername', novalidate)
//-.alert.alert-danger(ng-messages='changeUsername.$error && changeUsername.submitted') {{ $t('fillAll') }}
.form-group
input.form-control(type='text', :placeholder="$t('newUsername')", v-model='usernameUpdates.username', required)
.form-group
input.form-control(type='password', :placeholder="$t('password')", v-model='usernameUpdates.password', required)
button.btn.btn-primary(type='submit', @click='changeUser("username", usernameUpdates)') {{ $t('submit') }}
h5 {{ $t('changeEmail') }}
.form(v-if='user.auth.local', name='changeEmail', novalidate)
.form-group
input.form-control(type='text', :placeholder="$t('newEmail')", v-model='emailUpdates.newEmail', required)
.form-group
input.form-control(type='password', :placeholder="$t('password')", v-model='emailUpdates.password', required)
button.btn.btn-primary(type='submit', @click='changeUser("email", emailUpdates)') {{ $t('submit') }}
h5 {{ $t('changePass') }}
.form(v-if='user.auth.local', name='changePassword', novalidate)
.form-group
input.form-control(type='password', :placeholder="$t('oldPass')", v-model='passwordUpdates.password', required)
.form-group
input.form-control(type='password', :placeholder="$t('newPass')", v-model='passwordUpdates.newPassword', required)
.form-group
input.form-control(type='password', :placeholder="$t('confirmPass')", v-model='passwordUpdates.confirmPassword', required)
button.btn.btn-primary(type='submit', @click='changeUser("password", passwordUpdates)') {{ $t('submit') }}
div
h5 {{ $t('dangerZone') }}
div
button.btn.btn-danger(@click='openResetModal()',
popover-trigger='mouseenter', popover-placement='right', :popover="$t('resetAccPop')") {{ $t('resetAccount') }}
button.btn.btn-danger(@click='openDeleteModal()',
popover-trigger='mouseenter', :popover="$t('deleteAccPop')") {{ $t('deleteAccount') }}
</template>
<style scope>
.usersettings h5 {
margin-top: 1em;
}
</style>
<script>
import hello from 'hellojs';
import moment from 'moment';
import axios from 'axios';
import { mapState } from 'client/libs/store';
import restoreModal from './restoreModal';
import resetModal from './resetModal';
import deleteModal from './deleteModal';
import { SUPPORTED_SOCIAL_NETWORKS } from '../../../common/script/constants';
// @TODO: this needs our window.env fix
// import { availableLanguages } from '../../../server/libs/i18n';
export default {
components: {
restoreModal,
resetModal,
deleteModal,
},
data () {
let dayStartOptions = [];
for (let number = 0; number < 24; number += 1) {
let meridian = number < 12 ? 'AM' : 'PM';
let hour = number % 12;
let option = {
value: number,
name: `${hour ? hour : 12}:00 ${meridian}`,
};
dayStartOptions.push(option);
}
return {
SOCIAL_AUTH_NETWORKS: [],
party: {},
// @TODO: import
availableLanguages: [
{
code: 'en',
name: 'English',
},
],
availableFormats: ['MM/dd/yyyy', 'dd/MM/yyyy', 'yyyy/MM/dd'],
dayStartOptions,
newDayStart: 0,
usernameUpdates: {},
emailUpdates: {},
passwordUpdates: {},
};
},
mounted () {
this.SOCIAL_AUTH_NETWORKS = SUPPORTED_SOCIAL_NETWORKS;
// @TODO: We may need to request the party here
this.party = this.$store.state.party;
this.newDayStart = this.user.preferences.dayStart;
},
computed: {
...mapState({user: 'user.data'}),
timezoneOffsetToUtc () {
let offset = this.user.preferences.timezoneOffset;
let sign = offset > 0 ? '-' : '+';
offset = Math.abs(offset) / 60;
let hour = Math.floor(offset);
let minutesInt = (offset - hour) * 60;
let minutes = minutesInt < 10 ? `0${minutesInt}` : minutesInt;
return `UTC${sign}${hour}:${minutes}`;
},
selectedLanguage () {
return this.user.preferences.language;
},
dayStart () {
return this.user.preferences.dayStart;
},
},
methods: {
set (preferenceType, subtype) {
let settings = {};
if (!subtype) {
settings[`preferences.${preferenceType}`] = this.user.preferences[preferenceType];
} else {
settings[`preferences.${preferenceType}.${subtype}`] = this.user.preferences[preferenceType][subtype];
}
this.$store.dispatch('user:set', settings);
},
hideHeader () {
this.set('hideHeader');
if (!this.user.preferences.hideHeader || !this.user.preferences.stickyHeader) return;
this.user.preferences.hideHeader = false;
this.set('stickyHeader');
},
toggleStickyHeader () {
this.set('stickyHeader');
},
showTour () {
// @TODO: Do we still use this?
// User.set({'flags.showTour':true});
// Guide.goto('intro', 0, true);
},
showBailey () {
this.user.flags.newStuff = true;
this.set('flags', 'newStuff');
},
hasBackupAuthOption (networkKeyToCheck) {
if (this.user.auth.local.username) {
return true;
}
return find(this.SOCIAL_AUTH_NETWORKS, (network) => {
if (network.key !== networkKeyToCheck) {
if (this.user.auth.hasOwnProperty(network.key)) {
return this.user.auth[network.key].id;
}
}
});
},
calculateNextCron () {
let nextCron = moment().hours(this.newDayStart).minutes(0).seconds(0).milliseconds(0);
let currentHour = moment().format('H');
if (currentHour >= this.newDayStart) {
nextCron = nextCron.add(1, 'day');
}
return nextCron.format('x');
},
openDayStartModal () {
let nextCron = this.calculateNextCron();
// @TODO: Add generic modal
if (!confirm(`Are you sure you want to change cron? Next cron will be ${nextCron}`)) return;
this.saveDayStart();
// $rootScope.openModal('change-day-start', { scope: $scope });
},
async saveDayStart () {
this.user.preferences.dayStart = this.newDayStart;
await axios.post('/api/v3/user/custom-day-start', {
dayStart: this.newDayStart,
});
// @TODO
// Notification.text(response.data.data.message);
},
changeLanguage () {
this.user.preferences.language = this.selectedLanguage.code;
this.set('language');
},
async changeUser (attribute, updates) {
await axios.put(`/api/v3/user/auth/update-${attribute}`, updates);
alert(this.$t(`${attribute}Success`));
this.user[attribute] = updates[attribute];
updates = {};
},
openRestoreModal () {
this.$root.$emit('show::modal', 'restore');
},
openResetModal () {
this.$root.$emit('show::modal', 'reset');
},
openDeleteModal () {
this.$root.$emit('show::modal', 'delete');
},
async deleteSocialAuth (networkKey) {
// @TODO: What do we use this for?
// let networktoRemove = find(SOCIAL_AUTH_NETWORKS, function (network) {
// return network.key === networkKey;
// });
await axios.get(`/api/v3/user/auth/social/${networkKey}`);
// @TODO:
// Notification.text(env.t("detachedSocial", {network: network.name}));
// User.sync();
},
async socialAuth (network) {
let auth = await hello(network).login({scope: 'email'});
await this.$store.dispatch('auth:socialAuth', {
auth,
});
this.$router.go('/tasks');
},
},
};
</script>

View File

@@ -0,0 +1,427 @@
<template lang="pug">
.standard-page
amazon-payments-modal(:amazon-payments='amazonPayments')
h1 {{ $t('subscription') }}
.row
.col-6
h2 {{ $t('benefits') }}
ul
li
span.hint(:popover="$t('buyGemsGoldText', {gemCostTranslation})",
popover-trigger='mouseenter',
popover-placement='right') {{ $t('buyGemsGold') }}
span.badge.badge-success(v-if='subscription.key !== "basic_earned"') {{ $t('buyGemsGoldCap', buyGemsGoldCap) }}
li
span.hint(:popover="$t('retainHistoryText')", popover-trigger='mouseenter', popover-placement='right') {{ $t('retainHistory') }}
li
span.hint(:popover="$t('doubleDropsText')", popover-trigger='mouseenter', popover-placement='right') {{ $t('doubleDrops') }}
li
span.hint(:popover="$t('mysteryItemText')", popover-trigger='mouseenter', popover-placement='right') {{ $t('mysteryItem') }}
div(v-if='subscription.key !== "basic_earned"')
.badge.badge-success {{ $t('mysticHourglass', mysticHourglass) }}
.small.muted {{ $t('mysticHourglassText') }}
li
span.hint(:popover="$t('exclusiveJackalopePetText')", popover-trigger='mouseenter', popover-placement='right') {{ $t('exclusiveJackalopePet') }}
li
span.hint(:popover="$t('supportDevsText')", popover-trigger='mouseenter', popover-placement='right') {{ $t('supportDevs') }}
.col-6
h2 Plan
table.table.alert.alert-info(v-if='hasSubscription')
tr(v-if='hasCanceledSubscription'): td.alert.alert-warning
span.noninteractive-button.btn-danger {{ $t('canceledSubscription') }}
i.glyphicon.glyphicon-time
| {{ $t('subCanceled') }}
strong {{user.purchased.plan.dateTerminated | date}}
tr(v-if='!hasCanceledSubscription'): td
h4 {{ $t('subscribed') }}
p(v-if='hasPlan && !hasGroupPlan') {{ $t('purchasedPlanId', {purchasedPlanIdInfo}) }}
p(v-if='hasGroupPlan') {{ $t('youHaveGroupPlan') }}
tr(v-if='user.purchased.plan.extraMonths'): td
span.glyphicon.glyphicon-credit-card
| &nbsp; {{ $t('purchasedPlanExtraMonths', {purchasedPlanExtraMonthsDetails}) }}
tr(v-if='hasConsecutiveSubscription'): td
span.glyphicon.glyphicon-forward
| &nbsp; {{ $t('consecutiveSubscription') }}
ul.list-unstyled
li {{ $t('consecutiveMonths') }} {{user.purchased.plan.consecutive.count + user.purchased.plan.consecutive.offset}}
li {{ $t('gemCapExtra') }}} {{user.purchased.plan.consecutive.gemCapExtra}}
li {{ $t('mysticHourglasses') }} {{user.purchased.plan.consecutive.trinkets}}
div(v-if='!hasSubscription || hasCanceledSubscription')
h4(v-if='hasCanceledSubscription') {{ $t("resubscribe") }}
.form-group.reduce-top-margin
.radio(v-for='block in subscriptionBlocksOrdered', v-if="block.target !== 'group' && block.canSubscribe === true")
label
input(type="radio", name="subRadio", :value="block.key", v-model='subscription.key')
span(v-if='block.original')
span.label.label-success.line-through
| ${{block.original }}
| {{ $t('subscriptionRateText', {price: block.price, months: block.months}) }}
span(v-if='!block.original')
| {{ $t('subscriptionRateText', {price: block.price, months: block.months}) }}
.form-inline
.form-group
input.form-control(type='text', v-model='subscription.coupon', :placeholder="$t('couponPlaceholder')")
.form-group
button.btn.btn-primary(type='button', @click='applyCoupon(subscription.coupon)') {{ $t("apply") }}
div(v-if='hasSubscription')
.btn.btn-primary(v-if='canEditCardDetails', @click='showStripeEdit()') {{ $t('subUpdateCard') }}
.btn.btn-sm.btn-danger(v-if='canCancelSubscription', @click='cancelSubscription()') {{ $t('cancelSub') }}
small(v-if='!canCancelSubscription', v-html='getCancelSubInfo()')
.subscribe-pay(v-if='!hasSubscription || hasCanceledSubscription')
h3 {{ $t('subscribeUsing') }}
.row.text-center
.col-md-4
button.purchase.btn.btn-primary(@click='showStripe({subscription:subscription.key, coupon:subscription.coupon})', :disabled='!subscription.key') {{ $t('card') }}
.col-md-4
a.purchase(:href='paypalPurchaseLink', :disabled='!subscription.key', target='_blank')
img(src='https://www.paypalobjects.com/webstatic/en_US/i/buttons/pp-acceptance-small.png', :alt="$t('paypal')")
.col-md-4
a.purchase(@click="amazonPaymentsInit({type: 'subscription', subscription:subscription.key, coupon:subscription.coupon})")
img(src='https://payments.amazon.com/gp/cba/button', :alt="$t('amazonPayments')")
.row
.col-6
h2 {{ $t('giftSubscription') }}
ol
li {{ $t('giftSubscriptionText1') }}
li {{ $t('giftSubscriptionText2') }}
li {{ $t('giftSubscriptionText3') }}
h4 {{ $t('giftSubscriptionText4') }}
</template>
<style scoped>
.badge.badge-success {
color: #fff;
}
.subscribe-pay {
margin-top: 1em;
}
</style>
<script>
import axios from 'axios';
import moment from 'moment';
import filter from 'lodash/filter';
import sortBy from 'lodash/sortBy';
import min from 'lodash/min';
import { mapState } from 'client/libs/store';
import subscriptionBlocks from '../../../common/script/content/subscriptionBlocks';
import planGemLimits from '../../../common/script/libs/planGemLimits';
import amazonPaymentsModal from '../payments/amazonModal';
const STRIPE_PUB_KEY = 'pk_test_6pRNASCoBOKtIshFeQd4XMUh';
export default {
components: {
amazonPaymentsModal,
},
data () {
return {
gemCostTranslation: {
gemCost: planGemLimits.convRate,
gemLimit: planGemLimits.convRate,
},
subscription: {
key: 'basic_earned',
},
amazonPayments: {},
StripeCheckout: {},
paymentMethods: {
AMAZON_PAYMENTS: 'Amazon Payments',
STRIPE: 'Stripe',
GOOGLE: 'Google',
APPLE: 'Apple',
PAYPAL: 'Paypal',
GIFT: 'Gift',
},
};
},
mounted () {
this.StripeCheckout = window.StripeCheckout;
},
filters: {
date (value) {
if (!value) return '';
return moment(value).formate(this.user.preferences.dateFormat);
},
},
computed: {
...mapState({user: 'user.data'}),
paypalPurchaseLink () {
let couponString = '';
if (this.subscription.coupon) couponString = `&coupon=${this.subscription.coupon}`;
return `/paypal/subscribe?_id=${this.user._id}&apiToken=${this.user.apiToken}&sub=${this.subscription.key}${couponString}`;
},
subscriptionBlocksOrdered () {
let subscriptions = filter(subscriptionBlocks, (o) => {
return o.discount !== true;
});
return sortBy(subscriptions, [(o) => {
return o.months;
}]);
},
purchasedPlanIdInfo () {
return {
price: this.subscriptionBlocks[this.user.purchased.plan.planId].price,
months: this.subscriptionBlocks[this.user.purchased.plan.planId].months,
plan: this.user.purchased.plan.paymentMethod,
};
},
subscriptionBlocks () {
return subscriptionBlocks;
},
canEditCardDetails () {
return Boolean(
!this.hasCanceledSubscription &&
this.user.purchased.plan.paymentMethod === this.paymentMethods.STRIPE
);
},
hasSubscription () {
return Boolean(this.user.purchased.plan.customerId);
},
hasCanceledSubscription () {
return (
this.hasSubscription &&
Boolean(this.user.purchased.plan.dateTerminated)
);
},
hasPlan () {
return Boolean(this.user.purchased.plan.planId);
},
hasGroupPlan () {
return this.user.purchased.plan.customerId === 'group-plan';
},
hasConsecutiveSubscription () {
return Boolean(this.user.purchased.plan.consecutive.count) || Boolean(this.user.purchased.plan.consecutive.offset);
},
purchasedPlanExtraMonthsDetails () {
return {
months: this.user.purchased.plan.extraMonths.toFixed(2),
};
},
buyGemsGoldCap () {
return {
amount: min(this.gemGoldCap),
};
},
gemGoldCap () {
let baseCap = 25;
let gemCapIncrement = 5;
let capIncrementThreshold = 3;
let gemCapExtra = this.user.purchased.plan.consecutive.gemCapExtra;
let blocks = subscriptionBlocks[this.subscription.key].months / capIncrementThreshold;
let flooredBlocks = Math.floor(blocks);
let userTotalDropCap = baseCap + gemCapExtra + flooredBlocks * gemCapIncrement;
let maxDropCap = 50;
return [userTotalDropCap, maxDropCap];
},
numberOfMysticHourglasses () {
let numberOfHourglasses = subscriptionBlocks[this.subscription.key].months / 3;
return Math.floor(numberOfHourglasses);
},
mysticHourglass () {
return {
amount: this.numberOfMysticHourglasses,
};
},
},
methods: {
async applyCoupon (coupon) {
let response = await axios.get(`/api/v3/coupons/validate/${coupon}`);
if (!response.data.valid) {
// Notification.error(env.t('invalidCoupon'), true);
return;
}
// Notification.text("Coupon applied!");
let subs = subscriptionBlocks;
subs.basic_6mo.discount = true;
subs.google_6mo.discount = false;
},
showStripe (data) {
if (!this.checkGemAmount(data)) return;
let sub = false;
if (data.subscription) {
sub = data.subscription;
} else if (data.gift && data.gift.type === 'subscription') {
sub = data.gift.subscription.key;
}
sub = sub && subscriptionBlocks[sub];
let amount = 500;// 500 = $5
if (sub) amount = sub.price * 100;
if (data.gift && data.gift.type === 'gems') amount = data.gift.gems.amount / 4 * 100;
if (data.group) amount = (sub.price + 3 * (data.group.memberCount - 1)) * 100;
this.StripeCheckout.open({
key: STRIPE_PUB_KEY,
address: false,
amount,
name: 'Habitica',
description: sub ? this.$t('subscribe') : this.$t('checkout'),
image: '/apple-touch-icon-144-precomposed.png',
panelLabel: sub ? this.$t('subscribe') : this.$t('checkout'),
token: async (res) => {
let url = '/stripe/checkout?a=a'; // just so I can concat &x=x below
if (data.groupToCreate) {
url = '/api/v3/groups/create-plan?a=a';
res.groupToCreate = data.groupToCreate;
res.paymentType = 'Stripe';
}
if (data.gift) url += `&gift=${this.encodeGift(data.uuid, data.gift)}`;
if (data.subscription) url += `&sub=${sub.key}`;
if (data.coupon) url += `&coupon=${data.coupon}`;
if (data.groupId) url += `&groupId=${data.groupId}`;
let response = await axios.post(url, res);
// Success
if (response && response.data && response.data._id) {
this.$router.push(`/#/options/groups/guilds/${response.data._id}`);
} else {
window.location.reload(true);
}
// Error
alert(response.message);
},
});
},
showStripeEdit (config) {
let groupId;
if (config && config.groupId) {
groupId = config.groupId;
}
this.StripeCheckout.open({
key: STRIPE_PUB_KEY,
address: false,
name: this.$t('subUpdateTitle'),
description: this.$t('subUpdateDescription'),
panelLabel: this.$t('subUpdateCard'),
token: async (data) => {
data.groupId = groupId;
let url = '/stripe/subscribe/edit';
let response = await axios.post(url, data);
// Succss
window.location.reload(true);
// error
alert(response.message);
},
});
},
canCancelSubscription () {
return (
this.user.purchased.plan.paymentMethod !== this.paymentMethods.GOOGLE &&
this.user.purchased.plan.paymentMethod !== this.paymentMethods.APPLE &&
!this.hasCanceledSubscription &&
!this.hasGroupPlan
);
},
async cancelSubscription (config) {
if (config && config.group && !confirm(this.$t('confirmCancelGroupPlan'))) return;
if (!confirm(this.$t('sureCancelSub'))) return;
let group;
if (config && config.group) {
group = config.group;
}
let paymentMethod = this.user.purchased.plan.paymentMethod;
if (group) {
paymentMethod = group.purchased.plan.paymentMethod;
}
if (paymentMethod === 'Amazon Payments') {
paymentMethod = 'amazon';
} else {
paymentMethod = paymentMethod.toLowerCase();
}
let queryParams = {
_id: this.user._id,
apiToken: this.user.apiToken,
noRedirect: true,
};
if (group) {
queryParams.groupId = group._id;
}
let cancelUrl = `/${paymentMethod}/subscribe/cancel?${$.param(queryParams)}`;
await axios.get(cancelUrl);
// Success
alert(this.$t('paypalCanceled'));
this.$router.push('/');
},
getCancelSubInfo () {
return this.$t(`cancelSubInfo${this.user.purchased.plan.paymentMethod}`);
},
amazonPaymentsInit (data) {
if (!this.isAmazonReady) return;
if (!this.checkGemAmount(data)) return;
if (data.type !== 'single' && data.type !== 'subscription') return;
if (data.gift) {
if (data.gift.gems && data.gift.gems.amount && data.gift.gems.amount <= 0) return;
data.gift.uuid = data.giftedTo;
}
if (data.subscription) {
this.amazonPayments.subscription = data.subscription;
this.amazonPayments.coupon = data.coupon;
}
if (data.groupId) {
this.amazonPayments.groupId = data.groupId;
}
if (data.groupToCreate) {
this.amazonPayments.groupToCreate = data.groupToCreate;
}
this.amazonPayments.gift = data.gift;
this.amazonPayments.type = data.type;
this.$root.$emit('show::modal', 'amazon-payment');
},
payPalPayment (data) {
if (!this.checkGemAmount(data)) return;
let gift = this.encodeGift(data.giftedTo, data.gift);
let url = `/paypal/checkout?_id=${this.user._id}&apiToken=${this.user.apiToken}&gift=${gift}`;
axios.get(url);
},
encodeGift (uuid, gift) {
gift.uuid = uuid;
let encodedString = JSON.stringify(gift);
return encodeURIComponent(encodedString);
},
checkGemAmount (data) {
let isGem = data && data.gift && data.gift.type === 'gems';
let notEnoughGem = isGem && (!data.gift.gems.amount || data.gift.gems.amount === 0);
if (notEnoughGem) {
Notification.error(this.$t('badAmountOfGemsToPurchase'), true);
return false;
}
return true;
},
},
};
</script>

View File

@@ -9,5 +9,11 @@
<body> <body>
<div id="app"></div> <div id="app"></div>
<!-- built files will be auto injected --> <!-- built files will be auto injected -->
<script async type='text/javascript'
src='https://static-na.payments-amazon.com/OffAmazonPayments/us/sandbox/js/Widgets.js'>
</script>
<script src="https://checkout.stripe.com/v2/checkout.js"></script>
</script>
</body> </body>
</html> </html>

View File

@@ -19,6 +19,15 @@ const StatsPage = () => import(/* webpackChunkName: "user" */'./components/userM
const AchievementsPage = () => import(/* webpackChunkName: "user" */'./components/userMenu/achievements'); const AchievementsPage = () => import(/* webpackChunkName: "user" */'./components/userMenu/achievements');
const ProfilePage = () => import(/* webpackChunkName: "user" */'./components/userMenu/profile'); const ProfilePage = () => import(/* webpackChunkName: "user" */'./components/userMenu/profile');
// Settings
const Settings = () => import(/* webpackChunkName: "settings" */'./components/settings/index');
const API = () => import(/* webpackChunkName: "settings" */'./components/settings/api');
const DataExport = () => import(/* webpackChunkName: "settings" */'./components/settings/dataExport');
const Notifications = () => import(/* webpackChunkName: "settings" */'./components/settings/notifications');
const PromoCode = () => import(/* webpackChunkName: "settings" */'./components/settings/promoCode');
const Site = () => import(/* webpackChunkName: "settings" */'./components/settings/site');
const Subscription = () => import(/* webpackChunkName: "settings" */'./components/settings/subscription');
// Except for tasks that are always loaded all the other main level // Except for tasks that are always loaded all the other main level
// All the main level // All the main level
// components are loaded in separate webpack chunks. // components are loaded in separate webpack chunks.
@@ -123,7 +132,43 @@ const router = new VueRouter({
{ name: 'stats', path: 'stats', component: StatsPage }, { name: 'stats', path: 'stats', component: StatsPage },
{ name: 'achievements', path: 'achievements', component: AchievementsPage }, { name: 'achievements', path: 'achievements', component: AchievementsPage },
{ name: 'profile', path: 'profile', component: ProfilePage }, { name: 'profile', path: 'profile', component: ProfilePage },
{ name: 'settings', path: 'settings', component: Page }, {
name: 'settings',
path: 'settings',
component: Settings,
children: [
{
name: 'site',
path: 'site',
component: Site,
},
{
name: 'api',
path: 'api',
component: API,
},
{
name: 'dataExport',
path: 'data-export',
component: DataExport,
},
{
name: 'promoCode',
path: 'promo-code',
component: PromoCode,
},
{
name: 'subscription',
path: 'subscription',
component: Subscription,
},
{
name: 'notifications',
path: 'notifications',
component: Notifications,
},
],
},
], ],
}, },
], ],

View File

@@ -30,3 +30,18 @@ export function set (store, changes) {
export function sleep () { export function sleep () {
// @TODO: Implemented // @TODO: Implemented
} }
export async function addWebhook (store, payload) {
let response = await axios.post('/api/v3/user/webhook', payload.webhookInfo);
return response.data.data;
}
export async function updateWebhook (store, payload) {
let response = await axios.put(`/api/v3/user/webhook/${payload.webhook.id}`, payload.webhook);
return response.data.data;
}
export async function deleteWebhook (store, payload) {
let response = await axios.delete(`/api/v3/user/webhook/${payload.webhook.id}`);
return response.data.data;
}