mirror of
https://github.com/HabitRPG/habitica.git
synced 2025-12-17 06:37:23 +01:00
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:
@@ -38,6 +38,18 @@ module.exports = {
|
||||
target: 'http://localhost:3000',
|
||||
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"
|
||||
// with this option, according to the CSS-Loader README
|
||||
|
||||
@@ -51,7 +51,7 @@ div
|
||||
router-link.dropdown-item(:to="{name: 'stats'}") {{ $t('stats') }}
|
||||
router-link.dropdown-item(:to="{name: 'achievements'}") {{ $t('achievements') }}
|
||||
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') }}
|
||||
</template>
|
||||
|
||||
|
||||
201
website/client/components/payments/amazonModal.vue
Normal file
201
website/client/components/payments/amazonModal.vue
Normal 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>
|
||||
118
website/client/components/settings/api.vue
Normal file
118
website/client/components/settings/api.vue
Normal 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>
|
||||
13
website/client/components/settings/dataExport.vue
Normal file
13
website/client/components/settings/dataExport.vue
Normal 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>
|
||||
52
website/client/components/settings/deleteModal.vue
Normal file
52
website/client/components/settings/deleteModal.vue
Normal 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>
|
||||
23
website/client/components/settings/index.vue
Normal file
23
website/client/components/settings/index.vue
Normal 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>
|
||||
74
website/client/components/settings/notifications.vue
Normal file
74
website/client/components/settings/notifications.vue
Normal 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>
|
||||
61
website/client/components/settings/promoCode.vue
Normal file
61
website/client/components/settings/promoCode.vue
Normal 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>
|
||||
36
website/client/components/settings/resetModal.vue
Normal file
36
website/client/components/settings/resetModal.vue
Normal 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>
|
||||
109
website/client/components/settings/restoreModal.vue
Normal file
109
website/client/components/settings/restoreModal.vue
Normal 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>
|
||||
371
website/client/components/settings/site.vue
Normal file
371
website/client/components/settings/site.vue
Normal 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') }}
|
||||
|
|
||||
a(href='/#/options/profile/profile') {{ $t('loginNameDescription2') }}
|
||||
|
|
||||
| {{ $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>
|
||||
427
website/client/components/settings/subscription.vue
Normal file
427
website/client/components/settings/subscription.vue
Normal 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
|
||||
| {{ $t('purchasedPlanExtraMonths', {purchasedPlanExtraMonthsDetails}) }}
|
||||
tr(v-if='hasConsecutiveSubscription'): td
|
||||
span.glyphicon.glyphicon-forward
|
||||
| {{ $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>
|
||||
@@ -9,5 +9,11 @@
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<!-- 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>
|
||||
</html>
|
||||
|
||||
@@ -19,6 +19,15 @@ const StatsPage = () => import(/* webpackChunkName: "user" */'./components/userM
|
||||
const AchievementsPage = () => import(/* webpackChunkName: "user" */'./components/userMenu/achievements');
|
||||
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
|
||||
// All the main level
|
||||
// components are loaded in separate webpack chunks.
|
||||
@@ -123,7 +132,43 @@ const router = new VueRouter({
|
||||
{ name: 'stats', path: 'stats', component: StatsPage },
|
||||
{ name: 'achievements', path: 'achievements', component: AchievementsPage },
|
||||
{ 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,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -30,3 +30,18 @@ export function set (store, changes) {
|
||||
export function sleep () {
|
||||
// @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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user