Files
habitica/website/client/src/components/admin/admin-panel/user-support/subscriptionAndPerks.vue
Phillip Thelen e9829b8b60 Phillip/admin deleter (#15466)
* refactor sending jobs to worker server

* remove unused imports

* add delete button to adminpanel

* June 2025 content build (#15437)

* chore: June 2025 content build

* chore: typo fixing

* chore: corrections to summer 2025 mage armor, spritesheet

* fix(css): rebuild spritesmith-main

---------

Co-authored-by: Kalista Payne <sabrecat@gmail.com>

* fix(script): don't use extremely costly regex

* fix(logging): don't spam empty error events

* Translated using Weblate (Ukrainian)

Currently translated at 100.0% (134 of 134 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (280 of 280 strings)

Translated using Weblate (French)

Currently translated at 100.0% (280 of 280 strings)

Translated using Weblate (Spanish)

Currently translated at 99.6% (279 of 280 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 97.4% (840 of 862 strings)

Translated using Weblate (German)

Currently translated at 99.8% (907 of 908 strings)

Translated using Weblate (Dutch)

Currently translated at 79.3% (219 of 276 strings)

Translated using Weblate (Dutch)

Currently translated at 28.1% (69 of 245 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 97.4% (840 of 862 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 97.5% (402 of 412 strings)

Translated using Weblate (Dutch)

Currently translated at 91.5% (377 of 412 strings)

Translated using Weblate (Dutch)

Currently translated at 85.2% (774 of 908 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (91 of 91 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (908 of 908 strings)

Translated using Weblate (Slovak)

Currently translated at 63.4% (106 of 167 strings)

Translated using Weblate (Hungarian)

Currently translated at 100.0% (908 of 908 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (908 of 908 strings)

Translated using Weblate (Slovak)

Currently translated at 2.0% (5 of 245 strings)

Translated using Weblate (French)

Currently translated at 100.0% (908 of 908 strings)

Translated using Weblate (Russian)

Currently translated at 64.4% (158 of 245 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 97.0% (837 of 862 strings)

Translated using Weblate (German)

Currently translated at 97.9% (844 of 862 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (91 of 91 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 97.3% (401 of 412 strings)

Translated using Weblate (Portuguese)

Currently translated at 95.3% (393 of 412 strings)

Translated using Weblate (Slovak)

Currently translated at 45.6% (413 of 905 strings)

Translated using Weblate (Slovak)

Currently translated at 50.8% (85 of 167 strings)

Translated using Weblate (Russian)

Currently translated at 99.1% (113 of 114 strings)

Translated using Weblate (Russian)

Currently translated at 64.0% (157 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 64.0% (157 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 62.0% (152 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 62.0% (152 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 60.8% (149 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 60.8% (149 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 60.4% (148 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 60.4% (148 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 60.0% (147 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 60.0% (147 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 57.9% (142 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 57.9% (142 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 56.7% (139 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 56.7% (139 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 56.3% (138 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 56.3% (138 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 53.8% (132 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 53.8% (132 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 53.4% (131 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 53.4% (131 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 48.9% (120 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 48.9% (120 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 48.5% (119 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 48.5% (119 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 46.9% (115 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 46.9% (115 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 46.9% (115 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 46.9% (115 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 46.9% (115 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 46.9% (115 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 46.9% (115 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 46.9% (115 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 46.9% (115 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 46.9% (115 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 46.9% (115 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 46.9% (115 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 46.9% (115 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 45.3% (111 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 45.3% (111 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 45.3% (111 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 45.3% (111 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 44.4% (109 of 245 strings)

Translated using Weblate (German)

Currently translated at 99.9% (3324 of 3325 strings)

Translated using Weblate (Russian)

Currently translated at 44.4% (109 of 245 strings)

Translated using Weblate (Russian)

Currently translated at 44.4% (109 of 245 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (91 of 91 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (94 of 94 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 93.8% (107 of 114 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (22 of 22 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.7% (429 of 430 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 95.1% (820 of 862 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.6% (902 of 905 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (167 of 167 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (167 of 167 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 95.1% (820 of 862 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 95.1% (820 of 862 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 95.1% (820 of 862 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 93.8% (107 of 114 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 93.6% (3114 of 3325 strings)

Translated using Weblate (Portuguese)

Currently translated at 53.9% (1793 of 3325 strings)

Translated using Weblate (Dutch)

Currently translated at 78.1% (2600 of 3325 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.5% (242 of 243 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 95.1% (820 of 862 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 96.6% (398 of 412 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.6% (902 of 905 strings)

Translated using Weblate (Italian)

Currently translated at 99.1% (113 of 114 strings)

Translated using Weblate (Italian)

Currently translated at 87.3% (2903 of 3325 strings)

Translated using Weblate (Italian)

Currently translated at 17.1% (42 of 245 strings)

Translated using Weblate (Italian)

Currently translated at 99.0% (408 of 412 strings)

Translated using Weblate (Italian)

Currently translated at 92.7% (102 of 110 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 99.0% (3292 of 3325 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 98.7% (3285 of 3325 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 98.7% (3285 of 3325 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (134 of 134 strings)

Translated using Weblate (Slovak)

Currently translated at 100.0% (412 of 412 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (91 of 91 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (905 of 905 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 98.1% (3262 of 3325 strings)

Co-authored-by: Andrea <goffopaguro@gmail.com>
Co-authored-by: Artem StolyROV <stolyarov11303@gmail.com>
Co-authored-by: Céu <marcel.ufscar@gmail.com>
Co-authored-by: David Kaya <david@kaya.sk>
Co-authored-by: Filip Betko <filipbetko@gmail.com>
Co-authored-by: FingerTiao <787170918@qq.com>
Co-authored-by: Irina  Shcherbinina <cat3dcat007@gmail.com>
Co-authored-by: Jaime Martí <jaumemarti77@icloud.com>
Co-authored-by: Mencius <beautyalinap@gmail.com>
Co-authored-by: Natalie Luhrs <eilatan@gmail.com>
Co-authored-by: Nikita Maximov <ruvemaximus@gmail.com>
Co-authored-by: Sophie LE MASLE <sophiesuff@gmail.com>
Co-authored-by: Summer_GUI <heyang94@163.com>
Co-authored-by: Tetiana <merekka13@gmail.com>
Co-authored-by: Tom <tompsognathus@gmail.com>
Co-authored-by: Toro Mor <thomas.bizer@gmx.de>
Co-authored-by: V Aar <v.vanderaar@gmail.com>
Co-authored-by: Viktor Révész <rviktor@ivankapal.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: razil <boss.razmarin@gmail.com>
Co-authored-by: Волкозмей <klippiky@gmail.com>
Co-authored-by: Данила Мальцев <maltsev-danila@inbox.ru>
Co-authored-by: Татьяна Куклева <klippiky@gmail.com>
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/achievements/sk/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/de/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/es/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/hu/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/sk/
Translate-URL: https://translate.habitica.com/projects/habitica/backgrounds/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/challenge/it/
Translate-URL: https://translate.habitica.com/projects/habitica/communityguidelines/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/content/it/
Translate-URL: https://translate.habitica.com/projects/habitica/content/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/content/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/content/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/content/sk/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/it/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/faq/sk/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/de/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/it/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/pt/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/gear/zh_Hans/
Translate-URL: https://translate.habitica.com/projects/habitica/generic/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/groups/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/es/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/fr/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/hu/
Translate-URL: https://translate.habitica.com/projects/habitica/limited/nl/
Translate-URL: https://translate.habitica.com/projects/habitica/loginincentives/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/sk/
Translate-URL: https://translate.habitica.com/projects/habitica/npc/uk/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/it/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/pets/ru/
Translate-URL: https://translate.habitica.com/projects/habitica/quests/pt_BR/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/de/
Translate-URL: https://translate.habitica.com/projects/habitica/questscontent/pt_BR/
Translation: Habitica/Achievements
Translation: Habitica/Backgrounds
Translation: Habitica/Challenge
Translation: Habitica/Communityguidelines
Translation: Habitica/Content
Translation: Habitica/Faq
Translation: Habitica/Gear
Translation: Habitica/Generic
Translation: Habitica/Groups
Translation: Habitica/Limited
Translation: Habitica/Loginincentives
Translation: Habitica/Npc
Translation: Habitica/Pets
Translation: Habitica/Quests
Translation: Habitica/Questscontent

* 5.36.4

* chore(deps): bump serialize-javascript in /website/client (#15395)

Bumps [serialize-javascript](https://github.com/yahoo/serialize-javascript) from 6.0.1 to 6.0.2.
- [Release notes](https://github.com/yahoo/serialize-javascript/releases)
- [Commits](https://github.com/yahoo/serialize-javascript/compare/v6.0.1...v6.0.2)

---
updated-dependencies:
- dependency-name: serialize-javascript
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(deps-dev): bump axios from 1.7.4 to 1.8.2 (#15401)

Bumps [axios](https://github.com/axios/axios) from 1.7.4 to 1.8.2.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.7.4...v1.8.2)

---
updated-dependencies:
- dependency-name: axios
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(deps): bump prismjs from 1.29.0 to 1.30.0 (#15403)

Bumps [prismjs](https://github.com/PrismJS/prism) from 1.29.0 to 1.30.0.
- [Release notes](https://github.com/PrismJS/prism/releases)
- [Changelog](https://github.com/PrismJS/prism/blob/master/CHANGELOG.md)
- [Commits](https://github.com/PrismJS/prism/compare/v1.29.0...v1.30.0)

---
updated-dependencies:
- dependency-name: prismjs
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(deps): bump @babel/runtime-corejs2 in /website/client (#15406)

Bumps [@babel/runtime-corejs2](https://github.com/babel/babel/tree/HEAD/packages/babel-runtime-corejs2) from 7.23.6 to 7.26.10.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.26.10/packages/babel-runtime-corejs2)

---
updated-dependencies:
- dependency-name: "@babel/runtime-corejs2"
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(deps): bump @babel/helpers in /website/client (#15407)

Bumps [@babel/helpers](https://github.com/babel/babel/tree/HEAD/packages/babel-helpers) from 7.23.6 to 7.26.10.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.26.10/packages/babel-helpers)

---
updated-dependencies:
- dependency-name: "@babel/helpers"
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(deps): bump @babel/runtime from 7.23.9 to 7.26.10 (#15410)

Bumps [@babel/runtime](https://github.com/babel/babel/tree/HEAD/packages/babel-runtime) from 7.23.9 to 7.26.10.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.26.10/packages/babel-runtime)

---
updated-dependencies:
- dependency-name: "@babel/runtime"
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* chore(deps): bump http-proxy-middleware in /website/client (#15427)

Bumps [http-proxy-middleware](https://github.com/chimurai/http-proxy-middleware) from 2.0.6 to 2.0.9.
- [Release notes](https://github.com/chimurai/http-proxy-middleware/releases)
- [Changelog](https://github.com/chimurai/http-proxy-middleware/blob/v2.0.9/CHANGELOG.md)
- [Commits](https://github.com/chimurai/http-proxy-middleware/compare/v2.0.6...v2.0.9)

---
updated-dependencies:
- dependency-name: http-proxy-middleware
  dependency-version: 2.0.9
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* Optimize database access for some use cases (#15444)

* optimize query when listing challenge tasks

* Optimize query for checking if user is party leader

* correct worker call

* remove unused priority

* fix tests

* don’t use body with delete

* add detailed information about sub payment for google and apple

* Support paypal details for subscription in admin panel

* stripe payment details

* fix imports

* fix tests

* fix deleting account

* begin building group admin panel

* fix convertig sub to group plan

* improve sub status display

* fix lint

* fix long line

* fix sub state display

* lint fix

* fix

* delete amplitude data by default

* improve searching for email in admin panel

* correctly call method

* move delete button in admin panel

* fix(lint): whitespace

* fix(style): indent

* fix(typo): humand

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Natalie <78037386+CuriousMagpie@users.noreply.github.com>
Co-authored-by: Kalista Payne <sabrecat@gmail.com>
Co-authored-by: Weblate <noreply@weblate.org>
Co-authored-by: Andrea <goffopaguro@gmail.com>
Co-authored-by: Artem StolyROV <stolyarov11303@gmail.com>
Co-authored-by: Céu <marcel.ufscar@gmail.com>
Co-authored-by: David Kaya <david@kaya.sk>
Co-authored-by: Filip Betko <filipbetko@gmail.com>
Co-authored-by: FingerTiao <787170918@qq.com>
Co-authored-by: Irina  Shcherbinina <cat3dcat007@gmail.com>
Co-authored-by: Jaime Martí <jaumemarti77@icloud.com>
Co-authored-by: Mencius <beautyalinap@gmail.com>
Co-authored-by: Natalie Luhrs <eilatan@gmail.com>
Co-authored-by: Nikita Maximov <ruvemaximus@gmail.com>
Co-authored-by: Sophie LE MASLE <sophiesuff@gmail.com>
Co-authored-by: Summer_GUI <heyang94@163.com>
Co-authored-by: Tetiana <merekka13@gmail.com>
Co-authored-by: Tom <tompsognathus@gmail.com>
Co-authored-by: Toro Mor <thomas.bizer@gmx.de>
Co-authored-by: V Aar <v.vanderaar@gmail.com>
Co-authored-by: Viktor Révész <rviktor@ivankapal.com>
Co-authored-by: razil <boss.razmarin@gmail.com>
Co-authored-by: Волкозмей <klippiky@gmail.com>
Co-authored-by: Данила Мальцев <maltsev-danila@inbox.ru>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Kalista Payne <kalista@habitica.com>
2025-09-09 16:41:03 -05:00

813 lines
25 KiB
Vue

<template>
<form
@submit.prevent="saveHero({ hero: {
_id: hero._id,
purchased: hero.purchased
}, msg: 'Subscription Perks' })"
>
<div class="card mt-2">
<div
class="card-header"
@click="expand = !expand"
>
<h3
class="mb-0 mt-0"
:class="{ 'open': expand }"
>
Subscription, Monthly Perks
<span
v-if="isSubscribed() && !isCancelled()"
class="text-success float-right ml-3"
>
Active
</span>
<span
v-else-if="isSubscribed() && isCancelled()"
class="text-success float-right ml-3"
>
Active until {{ dateFormat(hero.purchased.plan.dateTerminated) }}
</span>
<span
v-else-if="hero.purchased.plan.customerId && hero.purchased.plan.dateTerminated"
class="text-warning float-right ml-3"
>
Inactive
</span>
<b
v-if="hasUnsavedChanges && !expand"
class="text-warning float-right"
>
Unsaved changes
</b>
</h3>
</div>
<div
v-if="expand"
class="card-body"
>
<div
class="form-group row"
>
<label class="col-sm-3 col-form-label">
Payment method:
</label>
<div class="col-sm-9">
<input
v-if="!isRegularPaymentMethod"
v-model="hero.purchased.plan.paymentMethod"
class="form-control"
type="text"
>
<select
v-else
v-model="hero.purchased.plan.paymentMethod"
class="form-control"
type="text"
>
<option value="Group Plan">
Group Plan
</option>
<option value="Stripe">
Stripe
</option>
<option value="Apple">
Apple
</option>
<option value="Google">
Google
</option>
<option value="Amazon Payments">
Amazon
</option>
<option value="PayPal">
PayPal
</option>
<option value="Gift">
Gift
</option>
<option value="">
Clear out
</option>
</select>
</div>
</div>
<div
class="form-group row"
>
<label class="col-sm-3 col-form-label">
Payment schedule:
</label>
<div class="col-sm-9">
<input
v-if="!isRegularPlanId"
v-model="hero.purchased.plan.planId"
class="form-control"
type="text"
>
<select
v-else
v-model="hero.purchased.plan.planId"
class="form-control"
type="text"
>
<option value="basic_earned">
Monthly recurring
</option>
<option value="basic_3mo">
3 Months recurring
</option>
<option value="basic_6mo">
6 Months recurring
</option>
<option value="basic_12mo">
12 Months recurring
</option>
<option value="group_monthly">
Group Plan (legacy)
</option>
<option value="group_plan_auto">
Group Plan (auto)
</option>
<option value="">
Clear out
</option>
</select>
</div>
</div>
<div
class="form-group row"
>
<label class="col-sm-3 col-form-label">
Customer ID:
</label>
<div class="col-sm-9">
<input
v-model="hero.purchased.plan.customerId"
class="form-control"
type="text"
>
</div>
</div>
<div
v-if="hero.purchased.plan.planId === 'group_plan_auto'"
class="form-group row"
>
<label class="col-sm-3 col-form-label">
Group Plan Memberships:
</label>
<div class="col-sm-9 col-form-label">
<loading-spinner
v-if="!groupPlans"
dark-color="true"
/>
<b
v-else-if="groupPlans.length === 0"
class="text-danger col-form-label"
>User is not part of an active group plan!</b>
<div
v-for="group in groupPlans"
v-else
:key="group._id"
class="card mb-2"
>
<div class="card-body">
<h6 class="card-title">
<router-link
:to="{ name: 'groupAdminGroup', params: { groupId: group._id } }"
>
{{ group.name }}
</router-link>
<small class="float-right">{{ group._id }}</small>
</h6>
<p class="card-text">
<strong>Leader: </strong>
<a
v-if="group.leader !== hero._id"
@click="switchUser(group.leader)"
>{{ group.leader }}</a>
<strong
v-else
class="text-success"
>This user</strong>
</p>
<p class="card-text">
<strong>Members: </strong> {{ group.memberCount }}
</p>
</div>
</div>
</div>
</div>
<div
v-if="hero.purchased.plan.dateCreated"
class="form-group row"
>
<label class="col-sm-3 col-form-label">
Creation date:
</label>
<div class="col-sm-9">
<div class="input-group">
<input
v-model="hero.purchased.plan.dateCreated"
class="form-control"
type="text"
>
<div class="input-group-append">
<strong class="input-group-text">
{{ dateFormat(hero.purchased.plan.dateCreated) }}
</strong>
</div>
</div>
</div>
</div>
<div
v-if="hero.purchased.plan.dateCurrentTypeCreated"
class="form-group row"
>
<label class="col-sm-3 col-form-label">
Current sub start date:
</label>
<div class="col-sm-9">
<div class="input-group">
<input
v-model="hero.purchased.plan.dateCurrentTypeCreated"
class="form-control"
type="text"
>
<div class="input-group-append">
<strong class="input-group-text">
{{ dateFormat(hero.purchased.plan.dateCurrentTypeCreated) }}
</strong>
</div>
</div>
</div>
</div>
<div class="form-group row">
<label class="col-sm-3 col-form-label">
Termination date:
</label>
<div class="col-sm-9">
<div class="input-group">
<input
v-model="hero.purchased.plan.dateTerminated"
class="form-control"
type="text"
>
<div class="input-group-append">
<strong class="input-group-text">
{{ dateFormat(hero.purchased.plan.dateTerminated) }}
</strong>
<a
v-if="!hero.purchased.plan.dateTerminated && hero.purchased.plan.planId"
v-b-modal.sub_termination_modal
class="btn btn-danger"
href="#"
>
Terminate
</a>
</div>
</div>
<small
v-if="isSubscribed() && !isCancelled()"
class="text-success"
>
The subscription does not have a termination date and is active.
</small>
</div>
</div>
<div class="form-group row">
<label class="col-sm-3 col-form-label">
Cumulative months:
</label>
<div class="col-sm-9">
<input
v-model="hero.purchased.plan.cumulativeCount"
class="form-control"
type="number"
min="0"
step="1"
>
<small class="text-secondary">
Cumulative subscribed months across subscription periods.
</small>
</div>
</div>
<div class="form-group row">
<label class="col-sm-3 col-form-label">
Extra months:
</label>
<div class="col-sm-9">
<div class="input-group">
<input
v-model="hero.purchased.plan.extraMonths"
class="form-control"
type="number"
min="0"
step="any"
>
<div class="input-group-append">
<a
v-if="hero.purchased.plan.dateTerminated && hero.purchased.plan.extraMonths > 0"
class="btn btn-warning"
@click="applyExtraMonths"
>
Apply Credit
</a>
</div>
</div>
<small class="text-secondary">
Additional credit that is applied if a subscription is cancelled.
</small>
</div>
</div>
<div class="form-group row">
<label class="col-sm-3 col-form-label">
Received hourglass bonus:
</label>
<div class="col-sm-9">
<div class="input-group">
<input
v-model="hero.purchased.plan.hourglassPromoReceived"
class="form-control"
type="text"
>
<div class="input-group-append">
<strong class="input-group-text">
{{ dateFormat(hero.purchased.plan.hourglassPromoReceived) }}
</strong>
</div>
</div>
</div>
</div>
<div class="form-group row">
<label class="col-sm-3 col-form-label">
Mystic Hourglasses:
</label>
<div class="col-sm-9">
<input
v-model="hero.purchased.plan.consecutive.trinkets"
class="form-control"
type="number"
min="0"
step="1"
>
</div>
</div>
<div class="form-group row">
<label class="col-sm-3 col-form-label">
Gem cap increase:
</label>
<div class="col-sm-9">
<input
v-model="hero.purchased.plan.consecutive.gemCapExtra"
class="form-control"
type="number"
min="0"
max="26"
step="2"
>
</div>
</div>
<div class="form-group row">
<label class="col-sm-3 col-form-label">
Total Gem cap:
</label>
<strong class="col-sm-9 col-form-label">
{{ Number(hero.purchased.plan.consecutive.gemCapExtra) + 24 }}
</strong>
</div>
<div class="form-group row">
<label class="col-sm-3 col-form-label">
Gems bought this month:
</label>
<div class="col-sm-9">
<input
v-model="hero.purchased.plan.gemsBought"
class="form-control"
type="number"
min="0"
:max="hero.purchased.plan.consecutive.gemCapExtra + 24"
step="1"
>
</div>
</div>
<div class="form-group row">
<label class="col-sm-3 col-form-label">
Mystery Items:
</label>
<div class="col-sm-9 col-form-label">
<span v-if="hero.purchased.plan.mysteryItems.length > 0">
<span
v-for="(item, index) in hero.purchased.plan.mysteryItems"
:key="index"
>
<strong v-if="index < hero.purchased.plan.mysteryItems.length - 1">
{{ item }},
</strong>
<strong v-else> {{ item }} </strong>
</span>
</span>
<span v-else>
<strong>None</strong>
</span>
</div>
</div>
<div
v-if="!isConvertingToGroupPlan && hero.purchased.plan.planId !== 'group_plan_auto'"
class="form-group row"
>
<div class="offset-sm-3 col-sm-9">
<button
type="button"
class="btn btn-secondary btn-sm"
@click="beginGroupPlanConvert"
>
Begin converting to group plan subscription
</button>
</div>
</div>
<div
v-if="isConvertingToGroupPlan"
class="form-group row"
>
<label class="col-sm-3 col-form-label">
Group Plan group ID:
</label>
<div class="col-sm-9">
<input
v-model="groupPlanID"
class="form-control"
type="text"
>
</div>
</div>
<div class="form-group row">
<h2>Payment Details</h2>
</div>
<div class="form-group row">
<div class="offset-sm-3 col-sm-9 mb-3">
<button
type="button"
class="btn btn-secondary btn-sm"
@click="getSubscriptionPaymentDetails"
>
Get Subscription Payment Details
</button>
</div>
</div>
<div
v-if="paymentDetails"
>
<div
v-for="(value, key) in paymentDetails"
:key="key"
class="form-group row"
>
<label class="col-sm-3 col-form-label">
{{ getHumanReadablePaymentDetails(key).label }}:
<span
:id="`${key}_tooltip`"
v-b-tooltip.hover.right="getHumanReadablePaymentDetails(key).help"
class="info-icon"
>?</span>
</label>
<strong class="col-sm-9 col-form-label">
<span v-if="value === true">Yes</span>
<span v-else-if="value === false">No</span>
<span
v-else-if="value instanceof String && isDate(value)"
v-b-tooltip.hover="value"
>
{{ formatDate(value) }}
</span>
<span v-else-if="value === null">---</span>
<span v-else>{{ value }}</span>
</strong>
</div>
<div class="form-group row">
<div class="offset-sm-3 col-sm-9">
<a
v-if="hero.purchased.plan.paymentMethod === 'Google'"
class="btn btn-primary btn-sm"
target="_blank"
:href="playOrdersUrl"
>
Play Console
</a>
<a
v-else-if="hero.purchased.plan.paymentMethod === 'Paypal'"
class="btn btn-primary btn-sm"
target="_blank"
:href="'https://www.paypal.com/billing/subscriptions/' + paymentDetails.customerId"
>
PayPal Dashboard
</a>
<a
v-else-if="hero.purchased.plan.paymentMethod === 'Stripe'"
class="btn btn-primary btn-sm"
target="_blank"
:href="'https://dashboard.stripe.com/customers/' + paymentDetails.customerId"
>
Stripe Dashboard
</a>
</div>
</div>
</div>
</div>
<div
v-if="expand"
class="card-footer d-flex align-items-center justify-content-between"
>
<input
type="submit"
value="Save"
class="btn btn-primary mt-1"
@click="saveClicked"
>
<b
v-if="hasUnsavedChanges"
class="text-warning float-right"
>
Unsaved changes
</b>
</div>
</div>
<b-modal
id="sub_termination_modal"
title="Set Termination Date"
>
<p>
You can set the sub benefit termination date to today or to the last
day of the current billing cycle. Any extra subscription credit will
then be processed and automatically added onto the selected date.
</p>
<template #modal-footer>
<div
class="mt-3 btn btn-secondary"
@click="$bvModal.hide('sub_termination_modal')"
>
Close
</div>
<div
class="mt-3 btn btn-danger"
@click="terminateSubscription()"
>
Set to Today
</div>
<div
class="mt-3 btn btn-danger"
@click="terminateSubscription(todayWithRemainingCycle)"
>
Set to {{ todayWithRemainingCycle.utc().format('MM/DD/YYYY') }}
</div>
</template>
</b-modal>
</form>
</template>
<style lang="scss" scoped>
@import '@/assets/scss/colors.scss';
.form-group {
margin-bottom: 0.4rem;
}
.input-group-append {
width: auto;
.input-group-text {
border-bottom-right-radius: 2px;
border-top-right-radius: 2px;
font-weight: 600;
font-size: 0.8rem;
color: $gray-200;
}
}
.info-icon {
font-size: 0.8rem;
color: $purple-400;
cursor: pointer;
margin-left: 0.2rem;
background-color: $gray-500;
padding: 0.1rem 0.3rem;
border-radius: 0.2rem;
}
.info-icon:hover {
background-color: $purple-400;
color: white;
}
</style>
<script>
import isUUID from 'validator/es/lib/isUUID';
import moment from 'moment';
import { getPlanContext } from '@/../../common/script/cron';
import subscriptionBlocks from '@/../../common/script/content/subscriptionBlocks';
import saveHero from '../mixins/saveHero';
import LoadingSpinner from '@/components/ui/loadingSpinner';
const PLAY_CONSOLE_ORDERS_BASE_URL = import.meta.env.PLAY_CONSOLE_ORDERS_BASE_URL;
const humanReadablePaymentDetails = {
customerId: {
label: 'Customer ID',
help: 'The unique identifier for the customer in the payment system.',
},
purchaseDate: {
label: 'Purchase Date',
help: 'The date when the subscription was purchased or renewed.',
},
originalPurchaseDate: {
label: 'Original Purchase Date',
help: 'The date when the subscription was first purchased.',
},
productId: {
label: 'Product ID',
help: 'The identifier for the product associated with the subscription.',
},
transactionId: {
label: 'Transaction ID',
help: 'The unique identifier for the last transaction in the payment system.',
},
isCanceled: {
label: 'Is Canceled',
help: 'Indicates whether the subscription has been canceled by the user or the system.',
},
isExpired: {
label: 'Is Expired',
help: 'Indicates whether the subscription has expired. A cancelled subscription may still be active until the end of the billing cycle.',
},
expirationDate: {
label: 'Termination Date',
help: 'The date when the subscription will expire or has expired.',
},
nextPaymentDate: {
label: 'Next Payment Date',
help: 'The date when the next payment is due. If the subscription is canceled or expired, this may be null.',
},
lastPaymentDate: {
label: 'Last Payment Date',
help: 'The date when the lastpayment was made for the subscription.',
},
failedPayments: {
label: 'Failed Payments',
help: 'Number of times the payment failed for this subscription.',
},
};
export default {
components: {
LoadingSpinner,
},
mixins: [saveHero],
props: {
hero: {
type: Object,
required: true,
},
hasUnsavedChanges: {
type: Boolean,
required: true,
},
groupPlans: {
type: Array,
default: null,
},
},
data () {
return {
expand: false,
isConvertingToGroupPlan: false,
groupPlanID: '',
subscriptionBlocks,
paymentDetails: null,
};
},
computed: {
nextHourglassDate () {
const currentPlanContext = getPlanContext(this.hero, new Date());
if (!currentPlanContext.nextHourglassDate) return 'N/A';
return currentPlanContext.nextHourglassDate.format('MMMM YYYY');
},
isRegularPlanId () {
return this.subscriptionBlocks[this.hero.purchased.plan.planId] !== undefined;
},
isRegularPaymentMethod () {
return [
'groupPlan',
'Group Plan',
'Stripe',
'Apple',
'Google',
'Amazon Payments',
'PayPal',
'Gift',
].includes(this.hero.purchased.plan.paymentMethod);
},
todayWithRemainingCycle () {
const now = moment();
const monthCount = subscriptionBlocks[this.hero.purchased.plan.planId].months;
const terminationDate = moment(this.hero.purchased.plan.dateCurrentTypeCreated || new Date());
while (terminationDate.isBefore(now)) {
terminationDate.add(monthCount, 'months');
}
return terminationDate;
},
playOrdersUrl () {
return `${PLAY_CONSOLE_ORDERS_BASE_URL}${this.paymentDetails?.transactionId || ''}`;
},
},
methods: {
dateFormat (date) {
if (!date) {
return '--';
}
return moment(date).format('YYYY/MM/DD');
},
terminateSubscription (terminationDate) {
if (terminationDate) {
this.hero.purchased.plan.dateTerminated = terminationDate.utc().format();
} else {
this.hero.purchased.plan.dateTerminated = moment(new Date()).utc().format();
}
this.applyExtraMonths();
this.saveHero({ hero: this.hero, msg: 'Subscription Termination', reloadData: true });
},
applyExtraMonths () {
if (this.hero.purchased.plan.extraMonths > 0 || this.hero.purchased.plan.extraMonths !== '0') {
const date = moment(this.hero.purchased.plan.dateTerminated || new Date());
const extraMonths = Math.max(this.hero.purchased.plan.extraMonths, 0);
const extraDays = Math.ceil(30.5 * extraMonths);
this.hero.purchased.plan.dateTerminated = date.add(extraDays, 'days').utc().format();
this.hero.purchased.plan.extraMonths = 0;
}
},
beginGroupPlanConvert () {
this.isConvertingToGroupPlan = true;
this.hero.purchased.plan.owner = '';
},
getSubscriptionPaymentDetails () {
this.$store.dispatch('admin:getSubscriptionPaymentDetails', { userIdentifier: this.hero._id })
.then(details => {
if (details) {
this.paymentDetails = details;
} else {
alert('No payment details found.');
}
})
.catch(error => {
console.error('Error fetching subscription payment details:', error);
alert(`Failed to fetch payment details: ${error.message || 'Unknown error'}`);
});
},
saveClicked (e) {
e.preventDefault();
if (this.isConvertingToGroupPlan) {
if (!isUUID(this.groupPlanID)) {
alert('Invalid group ID');
return;
}
this.hero.purchased.plan.convertToGroupPlan = this.groupPlanID;
this.saveHero({ hero: this.hero, msg: 'Group Plan Subscription', reloadData: true });
} else {
this.saveHero({ hero: this.hero, msg: 'Subscription Perks', reloadData: true });
}
},
switchUser (id) {
if (window.confirm('Switch to this user?')) {
this.$emit('changeUserIdentifier', id);
}
},
getHumanReadablePaymentDetails (key) {
return humanReadablePaymentDetails[key] || { label: key, help: '' };
},
isDate (date) {
return moment(date).isValid();
},
formatDate (date) {
return date ? moment(date).format('MM/DD/YYYY') : '---';
},
isSubscribed () {
console.log(this.hero.purchased.plan.customerId, this.hero.purchased.plan.dateTerminated);
return this.hero.purchased.plan
&& this.hero.purchased.plan.customerId
&& this.hero.purchased.plan.planId
&& this.hero.purchased.plan.paymentMethod
&& (
!this.hero.purchased.plan.dateTerminated
|| moment(this.hero.purchased.plan.dateTerminated).isAfter(moment())
);
},
isCancelled () {
return this.hero.purchased.plan
&& this.hero.purchased.plan.dateTerminated
&& this.hero.purchased.plan.dateTerminated !== '';
},
},
};
</script>