mirror of
https://github.com/PretendoNetwork/website.git
synced 2026-07-29 22:28:31 -05:00
Merge branch 'feature-stripe-tiers' into feature-stripe-tiers-styling
This commit is contained in:
commit
d4ce74b11a
30
package-lock.json
generated
30
package-lock.json
generated
|
|
@ -18,13 +18,13 @@
|
|||
"express-handlebars": "^5.3.1",
|
||||
"express-locale": "^2.0.0",
|
||||
"fs-extra": "^9.1.0",
|
||||
"gmail-send": "^1.8.14",
|
||||
"got": "^11.8.2",
|
||||
"gray-matter": "^4.0.3",
|
||||
"kaitai-struct": "^0.9.0",
|
||||
"marked": "^4.0.10",
|
||||
"mongoose": "^6.4.0",
|
||||
"morgan": "^1.10.0",
|
||||
"nodemailer": "^6.7.5",
|
||||
"stripe": "^9.9.0",
|
||||
"trello": "^0.11.0",
|
||||
"uuid": "^8.3.2"
|
||||
|
|
@ -1954,15 +1954,6 @@
|
|||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/gmail-send": {
|
||||
"version": "1.8.14",
|
||||
"resolved": "https://registry.npmjs.org/gmail-send/-/gmail-send-1.8.14.tgz",
|
||||
"integrity": "sha512-hc+4Ej7ZJtw0G5sync10pmWkpPXIabkQ+p/a92lPPTXni3ChEU9sR2wxOvK6Hx+5Ou+2m9h1cVffWEgtR6Gzkw==",
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.21",
|
||||
"nodemailer": "^6.6.5"
|
||||
}
|
||||
},
|
||||
"node_modules/got": {
|
||||
"version": "11.8.2",
|
||||
"resolved": "https://registry.npmjs.org/got/-/got-11.8.2.tgz",
|
||||
|
|
@ -2700,11 +2691,6 @@
|
|||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||
},
|
||||
"node_modules/lodash.clonedeep": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
|
||||
|
|
@ -6006,15 +5992,6 @@
|
|||
"type-fest": "^0.20.2"
|
||||
}
|
||||
},
|
||||
"gmail-send": {
|
||||
"version": "1.8.14",
|
||||
"resolved": "https://registry.npmjs.org/gmail-send/-/gmail-send-1.8.14.tgz",
|
||||
"integrity": "sha512-hc+4Ej7ZJtw0G5sync10pmWkpPXIabkQ+p/a92lPPTXni3ChEU9sR2wxOvK6Hx+5Ou+2m9h1cVffWEgtR6Gzkw==",
|
||||
"requires": {
|
||||
"lodash": "^4.17.21",
|
||||
"nodemailer": "^6.6.5"
|
||||
}
|
||||
},
|
||||
"got": {
|
||||
"version": "11.8.2",
|
||||
"resolved": "https://registry.npmjs.org/got/-/got-11.8.2.tgz",
|
||||
|
|
@ -6538,11 +6515,6 @@
|
|||
"type-check": "~0.4.0"
|
||||
}
|
||||
},
|
||||
"lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||
},
|
||||
"lodash.clonedeep": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
|
||||
|
|
|
|||
|
|
@ -26,13 +26,13 @@
|
|||
"express-handlebars": "^5.3.1",
|
||||
"express-locale": "^2.0.0",
|
||||
"fs-extra": "^9.1.0",
|
||||
"gmail-send": "^1.8.14",
|
||||
"got": "^11.8.2",
|
||||
"gray-matter": "^4.0.3",
|
||||
"kaitai-struct": "^0.9.0",
|
||||
"marked": "^4.0.10",
|
||||
"mongoose": "^6.4.0",
|
||||
"morgan": "^1.10.0",
|
||||
"nodemailer": "^6.7.5",
|
||||
"stripe": "^9.9.0",
|
||||
"trello": "^0.11.0",
|
||||
"uuid": "^8.3.2"
|
||||
|
|
|
|||
|
|
@ -25,25 +25,51 @@
|
|||
.account-sidebar .user .username {
|
||||
margin: 0px;
|
||||
}
|
||||
.account-sidebar .user .tier-name {
|
||||
margin: 0px;
|
||||
line-height: 1.2em;
|
||||
border-radius: 1.2em;
|
||||
border-width: 5px;
|
||||
border-style: solid;
|
||||
}
|
||||
.account-sidebar .user .tier-level-0 {
|
||||
color:gray;
|
||||
border-color: gray;
|
||||
}
|
||||
.account-sidebar .user .tier-level-1 {
|
||||
color:red;
|
||||
border-color: red;
|
||||
}
|
||||
.account-sidebar .user .tier-level-2 {
|
||||
color:darkorange;
|
||||
border-color: darkorange;
|
||||
}
|
||||
.account-sidebar .user .tier-level-3 {
|
||||
color:gold;
|
||||
border-color: gold;
|
||||
}
|
||||
.account-sidebar .user .mii {
|
||||
width: 128px;
|
||||
height: 128px;
|
||||
border-radius: 100%;
|
||||
background: var(--btn-secondary);
|
||||
}
|
||||
.account-sidebar .user #download-cemu-files {
|
||||
.account-sidebar .buttons a {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
align-items: center;
|
||||
padding: 24px;
|
||||
padding: 20px 24px;
|
||||
background: #383f6b;
|
||||
margin: 24px 0 0;
|
||||
margin: 20px 0 0;
|
||||
text-decoration: none;
|
||||
}
|
||||
.account-sidebar .user #download-cemu-files p.download-caption {
|
||||
margin: 15px 0 0;
|
||||
.account-sidebar .buttons a svg {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.account-sidebar .user p.cemu-warning {
|
||||
.account-sidebar .buttons a p.caption {
|
||||
margin: 0;
|
||||
}
|
||||
.account-sidebar .buttons p.cemu-warning {
|
||||
margin: 4px 0 0;
|
||||
font-size: 0.7rem;
|
||||
color: var(--text-secondary);
|
||||
|
|
@ -115,13 +141,11 @@
|
|||
}
|
||||
|
||||
fieldset {
|
||||
position: relative;
|
||||
height: min-content;
|
||||
padding: 0;
|
||||
border: none;
|
||||
}
|
||||
fieldset:disabled form label {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.setting-card .server-selection {
|
||||
display: flex;
|
||||
|
|
@ -152,16 +176,18 @@ fieldset:disabled form label {
|
|||
color: var(--text);
|
||||
}
|
||||
|
||||
.setting-card #remove-discord-connection {
|
||||
.setting-card #link-discord-account {
|
||||
width: 100%;
|
||||
padding: 12px 48px;
|
||||
cursor: pointer;
|
||||
background: #383f6b;
|
||||
}
|
||||
|
||||
.setting-card.sign-in-history a button {
|
||||
.setting-card button {
|
||||
width: 100%;
|
||||
height: fit-content;
|
||||
padding: 12px 48px;
|
||||
align-self: flex-end;
|
||||
cursor: pointer;
|
||||
background: #383f6b;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,31 @@
|
|||
height: 1400px;
|
||||
}
|
||||
|
||||
.back-arrow {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
top: 36px;
|
||||
left: max(calc((100vw - 1590px) / 2), 2.5vw);
|
||||
padding: 6px 10px;
|
||||
background: var(--btn-secondary);
|
||||
border-radius: 24px;
|
||||
|
||||
transition: filter 150ms;
|
||||
text-decoration: none;
|
||||
color: var(--text);
|
||||
}
|
||||
.back-arrow:hover {
|
||||
filter: brightness(1.5)
|
||||
}
|
||||
.back-arrow svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
.back-arrow span {
|
||||
margin: 0 4px;
|
||||
}
|
||||
|
||||
.account-form-wrapper {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
|
|
@ -164,8 +189,10 @@ label.tier p.price span {
|
|||
|
||||
form .button-wrapper {
|
||||
grid-column: 2 / span 1;
|
||||
position: relative;
|
||||
margin-top: 24px;
|
||||
}
|
||||
form button {
|
||||
button {
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
display: block;
|
||||
|
|
@ -179,21 +206,101 @@ form button {
|
|||
padding: 12px;
|
||||
color: var(--text);
|
||||
width: 100%;
|
||||
margin-top: 24px;
|
||||
pointer-events: none;
|
||||
filter: brightness(0.75) saturate(0.75); /* not using opacity here 'cause in the mobile layout you would see the cards under it */
|
||||
|
||||
transition: filter 300ms;
|
||||
}
|
||||
form .tier-radio:checked ~ .button-wrapper button {
|
||||
pointer-events: all;
|
||||
cursor: pointer;
|
||||
filter: none;
|
||||
}
|
||||
form button.disabled {
|
||||
pointer-events: none;
|
||||
filter: brightness(0.75) saturate(0.75); /* not using opacity here 'cause in the mobile layout you would see the cards under it */
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
form button.unsubscribe {
|
||||
position: relative;
|
||||
background: none;
|
||||
color: var(--text-secondary);
|
||||
margin-top: 12px;
|
||||
padding: 0;
|
||||
}
|
||||
form button.unsubscribe.hidden {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
pointer-events: none;
|
||||
z-index: -1;
|
||||
}
|
||||
form button.unsubscribe:hover {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
div.unsub-modal-wrapper,
|
||||
div.switch-tier-modal-wrapper {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
|
||||
z-index: 10;
|
||||
}
|
||||
div.unsub-modal-wrapper.hidden,
|
||||
div.switch-tier-modal-wrapper.hidden {
|
||||
display: none;
|
||||
}
|
||||
div.unsub-modal,
|
||||
div.switch-tier-modal {
|
||||
background: #393b5f;
|
||||
padding: 48px;
|
||||
border-radius: 8px;
|
||||
text-align: left;
|
||||
width: min(660px, 90%);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
div.unsub-modal h1,
|
||||
div.switch-tier-modal h1 {
|
||||
margin-top: 0;
|
||||
}
|
||||
p.unsub-modal-caption,
|
||||
p.switch-tier-modal-caption {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
p.unsub-modal-caption span,
|
||||
p.switch-tier-modal-caption span {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.unsub-modal-button-wrapper,
|
||||
.switch-tier-modal-button-wrapper {
|
||||
margin-top: 24px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.unsub-modal-button-wrapper button,
|
||||
.switch-tier-modal-button-wrapper button {
|
||||
margin-left: 12px;
|
||||
width: fit-content;
|
||||
}
|
||||
.unsub-modal-button-wrapper button.cancel,
|
||||
.switch-tier-modal-button-wrapper button.cancel {
|
||||
background: none;
|
||||
}
|
||||
.unsub-modal-button-wrapper button.confirm,
|
||||
.switch-tier-modal-button-wrapper button.confirm {
|
||||
padding: 12px 24px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 900px) {
|
||||
.account-form-wrapper {
|
||||
width: min(500px, 100%);
|
||||
margin-bottom: 120px;
|
||||
margin-bottom: 172px;
|
||||
}
|
||||
|
||||
form {
|
||||
|
|
@ -215,16 +322,28 @@ form .tier-radio:checked ~ .button-wrapper button {
|
|||
form .button-wrapper::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
top: -24px;
|
||||
left: -100vw;
|
||||
width: 200vw;
|
||||
height: 150%;
|
||||
height: 300%;
|
||||
background: #111531;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 600px) {
|
||||
div.unsub-modal {
|
||||
padding: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 380px) {
|
||||
label.tier .tier-perks {
|
||||
width: 80%;
|
||||
}
|
||||
.back-arrow {
|
||||
padding: 6px;
|
||||
}
|
||||
.back-arrow span {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
110
public/assets/js/upgrade.js
Normal file
110
public/assets/js/upgrade.js
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
const buttons = {
|
||||
submit: document.getElementById('submitButton'),
|
||||
unsubModal: {
|
||||
show: document.getElementById('unsubModalShowButton'),
|
||||
close: document.getElementById('unsubModalCloseButton'),
|
||||
confirm: document.getElementById('unsubModalConfirmButton'),
|
||||
},
|
||||
switchTierModal: {
|
||||
show: document.getElementById('switchTierShowButton'),
|
||||
close: document.getElementById('switchTierCloseButton'),
|
||||
confirm: document.getElementById('switchTierConfirmButton'),
|
||||
},
|
||||
};
|
||||
|
||||
const currentTierID = document.querySelector('form').dataset.currentTier || undefined;
|
||||
|
||||
const currentTierElement = document.querySelector(`#${currentTierID}`) || undefined;
|
||||
|
||||
// if the condition is met, we disable the submit button and enable the unsubscribe button
|
||||
function conditionalSubmitButton(condition, target) {
|
||||
if (condition) {
|
||||
buttons.submit.innerText = 'Already subscribed to this tier';
|
||||
buttons.unsubModal.show.innerText = `Unsubscribe from ${currentTierElement.dataset.tierName}`;
|
||||
buttons.submit.disabled = true;
|
||||
buttons.submit.classList.add('disabled');
|
||||
buttons.unsubModal.show.classList.remove('hidden');
|
||||
} else {
|
||||
buttons.submit.classList.remove('disabled');
|
||||
buttons.unsubModal.show.classList.add('hidden');
|
||||
buttons.submit.disabled = false;
|
||||
buttons.submit.innerText = `Subscribe to ${target.dataset.tierName}`;
|
||||
}
|
||||
}
|
||||
|
||||
function submitForm(cancel) {
|
||||
const form = document.querySelector('form');
|
||||
|
||||
if (cancel) {
|
||||
form.action = '/account/stripe/unsubscribe';
|
||||
} else {
|
||||
const selectedTier = form.querySelector('input[type="radio"]:checked').value;
|
||||
form.action = `/account/stripe/checkout/${selectedTier}`;
|
||||
}
|
||||
|
||||
form.submit();
|
||||
}
|
||||
|
||||
// If the currect tier exists, select it from the list and disable the submit button.
|
||||
if (currentTierElement) {
|
||||
currentTierElement.click();
|
||||
conditionalSubmitButton(true);
|
||||
}
|
||||
|
||||
// If a tier is selected, conditionally enable the submit button.
|
||||
document.querySelector('form').addEventListener('change', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
// If the selected tier is the current tier, set the button to disabled. Else we enable the button
|
||||
conditionalSubmitButton(e.target.value === currentTierElement?.value, e.target);
|
||||
});
|
||||
|
||||
// handle the submit button
|
||||
buttons.submit.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
// If the user is already subscribed to another tier, we show the confirm modal, else if this is a new subscription we submit the form.
|
||||
if (currentTierElement) {
|
||||
const oldTierNameSpan = document.querySelector('.switch-tier-modal-caption span.oldtier');
|
||||
const newTierNameSpan = document.querySelector('.switch-tier-modal-caption span.newtier');
|
||||
oldTierNameSpan.innerText = currentTierElement.dataset.tierName;
|
||||
newTierNameSpan.innerText = document.querySelector('input[name="tier"]:checked').dataset.tierName;
|
||||
|
||||
document.querySelector('.switch-tier-modal-wrapper').classList.remove('hidden');
|
||||
} else {
|
||||
submitForm();
|
||||
}
|
||||
});
|
||||
|
||||
buttons.unsubModal.show.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const tierNameSpan = document.querySelector('.unsub-modal-caption span');
|
||||
tierNameSpan.innerText = currentTierElement.dataset.tierName;
|
||||
|
||||
// Show the unsubscribe modal
|
||||
document.querySelector('.unsub-modal-wrapper').classList.remove('hidden');
|
||||
});
|
||||
buttons.unsubModal.close.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
// Hide the unsubscribe modal
|
||||
document.querySelector('.unsub-modal-wrapper').classList.add('hidden');
|
||||
});
|
||||
buttons.unsubModal.confirm.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
submitForm(true);
|
||||
});
|
||||
|
||||
buttons.switchTierModal.close.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
// Hide the switch tier modal
|
||||
document.querySelector('.switch-tier-modal-wrapper').classList.add('hidden');
|
||||
});
|
||||
buttons.switchTierModal.confirm.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
submitForm(false);
|
||||
});
|
||||
|
|
@ -1,10 +1,14 @@
|
|||
const Trello =require('trello');
|
||||
const Trello = require('trello');
|
||||
const Stripe = require('stripe');
|
||||
const got = require('got');
|
||||
const config = require('../config.json');
|
||||
|
||||
const trello = new Trello(config.trello.api_key, config.trello.api_token);
|
||||
const stripe = new Stripe(config.stripe.secret_key);
|
||||
|
||||
const VALID_LIST_NAMES = ['Not Started', 'Started', 'Completed'];
|
||||
let cache;
|
||||
let trelloCache;
|
||||
let stripeDonationCache;
|
||||
|
||||
async function getTrelloCache() {
|
||||
const available = await trelloAPIAvailable();
|
||||
|
|
@ -15,19 +19,19 @@ async function getTrelloCache() {
|
|||
};
|
||||
}
|
||||
|
||||
if (!cache) {
|
||||
cache = await updateTrelloCache();
|
||||
if (!trelloCache) {
|
||||
trelloCache = await updateTrelloCache();
|
||||
}
|
||||
|
||||
if (cache.update_time < Date.now() - (1000 * 60 * 60)) {
|
||||
cache = await updateTrelloCache();
|
||||
if (trelloCache.update_time < Date.now() - (1000 * 60 * 60)) {
|
||||
trelloCache = await updateTrelloCache();
|
||||
}
|
||||
|
||||
return cache;
|
||||
return trelloCache;
|
||||
}
|
||||
|
||||
async function updateTrelloCache() {
|
||||
const progressData = {
|
||||
const progressCache = {
|
||||
update_time: Date.now(),
|
||||
sections: []
|
||||
};
|
||||
|
|
@ -70,11 +74,11 @@ async function updateTrelloCache() {
|
|||
}
|
||||
|
||||
if (meta.progress.not_started.length !== 0 || meta.progress.started.length !== 0 || meta.progress.completed.length !== 0) {
|
||||
progressData.sections.push(meta);
|
||||
progressCache.sections.push(meta);
|
||||
}
|
||||
}
|
||||
|
||||
return progressData;
|
||||
return progressCache;
|
||||
}
|
||||
|
||||
async function trelloAPIAvailable() {
|
||||
|
|
@ -82,7 +86,55 @@ async function trelloAPIAvailable() {
|
|||
return status.description === 'All Systems Operational';
|
||||
}
|
||||
|
||||
async function getStripeDonationCache() {
|
||||
if (!stripeDonationCache) {
|
||||
stripeDonationCache = await updateStripeDonationCache();
|
||||
}
|
||||
|
||||
if (stripeDonationCache.update_time < Date.now() - (1000 * 60 * 60)) {
|
||||
stripeDonationCache = await updateStripeDonationCache();
|
||||
}
|
||||
|
||||
return stripeDonationCache;
|
||||
}
|
||||
|
||||
async function updateStripeDonationCache() {
|
||||
const donationCache = {
|
||||
update_time: Date.now(),
|
||||
goal: config.stripe.goal_cents,
|
||||
total: 0,
|
||||
donators: 0,
|
||||
completed_real: 0,
|
||||
completed_capped: 0
|
||||
};
|
||||
|
||||
let hasMore = true;
|
||||
let lastId;
|
||||
|
||||
while (hasMore) {
|
||||
const { data: activeSubscriptions, has_more } = await stripe.subscriptions.list({
|
||||
limit: 100,
|
||||
status: 'active',
|
||||
starting_after: lastId
|
||||
});
|
||||
|
||||
donationCache.donators += activeSubscriptions.length;
|
||||
|
||||
for (const subscription of activeSubscriptions) {
|
||||
donationCache.total += subscription.plan.amount;
|
||||
lastId = subscription.id;
|
||||
}
|
||||
|
||||
hasMore = has_more;
|
||||
}
|
||||
|
||||
donationCache.completed_real = Math.floor((donationCache.total / donationCache.goal) * 100); // real completion amount
|
||||
donationCache.completed_capped = Math.max(0, Math.min(donationCache.completed_real, 100)); // capped at 100
|
||||
|
||||
return donationCache;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getTrelloCache,
|
||||
updateTrelloCache
|
||||
getStripeDonationCache
|
||||
};
|
||||
22
src/mailer.js
Normal file
22
src/mailer.js
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
const nodemailer = require('nodemailer');
|
||||
const config = require('../config.json');
|
||||
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: 'smtp.gmail.com',
|
||||
port: 587,
|
||||
secure: false,
|
||||
auth: {
|
||||
user: config.gmail.user,
|
||||
pass: config.gmail.pass
|
||||
}
|
||||
});
|
||||
|
||||
async function sendMail(options) {
|
||||
options.from = config.gmail.from;
|
||||
|
||||
return await transporter.sendMail(options);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
sendMail
|
||||
};
|
||||
|
|
@ -5,7 +5,9 @@ const { v4: uuidv4 } = require('uuid');
|
|||
const AdmZip = require('adm-zip');
|
||||
const Stripe = require('stripe');
|
||||
const database = require('../database');
|
||||
const cache = require('../cache');
|
||||
const util = require('../util');
|
||||
const logger = require('../logger');
|
||||
const config = require('../../config.json');
|
||||
|
||||
const { Router } = express;
|
||||
|
|
@ -28,31 +30,28 @@ router.get('/', async (request, response) => {
|
|||
return response.redirect('/account/login');
|
||||
}
|
||||
|
||||
const { upgrade_success } = request.query;
|
||||
console.log(upgrade_success);
|
||||
const stripe = {};
|
||||
if (upgrade_success === 'true') {
|
||||
stripe.showNotice = true;
|
||||
stripe.success = true;
|
||||
} else if (upgrade_success === 'false') {
|
||||
stripe.showNotice = true;
|
||||
stripe.error = true;
|
||||
}
|
||||
|
||||
// Setup the data to be sent to the handlebars renderer
|
||||
const renderData = {
|
||||
layout: 'main',
|
||||
locale: util.getLocale(request.locale.region, request.locale.language),
|
||||
localeString: request.locale.toString(),
|
||||
linked: request.cookies.linked,
|
||||
error: request.cookies.error,
|
||||
stripe: stripe
|
||||
success: request.cookies.success,
|
||||
error: request.cookies.error
|
||||
};
|
||||
|
||||
// Reset message cookies
|
||||
response.clearCookie('linked', { domain: '.pretendo.network' });
|
||||
response.clearCookie('success', { domain: '.pretendo.network' });
|
||||
response.clearCookie('error', { domain: '.pretendo.network' });
|
||||
|
||||
// Check for Stripe messages
|
||||
const { upgrade_success } = request.query;
|
||||
|
||||
if (upgrade_success === 'true') {
|
||||
renderData.success = 'Account upgraded successfully';
|
||||
} else if (upgrade_success === 'false') {
|
||||
renderData.error = 'Account upgrade failed';
|
||||
}
|
||||
|
||||
// Attempt to get user data
|
||||
let apiResponse = await util.apiGetRequest('/v1/user', {
|
||||
'Authorization': `${request.cookies.token_type} ${request.cookies.access_token}`
|
||||
|
|
@ -93,7 +92,12 @@ router.get('/', async (request, response) => {
|
|||
|
||||
// Set user account info to render data
|
||||
const account = apiResponse.body;
|
||||
const pid = account.pid;
|
||||
|
||||
const pnid = await database.PNID.findOne({ pid });
|
||||
|
||||
renderData.tierName = pnid.get('connections.stripe.tier_name');
|
||||
renderData.tierLevel = pnid.get('connections.stripe.tier_level');
|
||||
renderData.account = account;
|
||||
renderData.isTester = account.access_level > 0;
|
||||
|
||||
|
|
@ -375,7 +379,7 @@ router.get('/connect/discord', async (request, response) => {
|
|||
}
|
||||
}
|
||||
|
||||
response.cookie('linked', 'Discord', { domain: '.pretendo.network' }).redirect('/account');
|
||||
response.cookie('success', 'Discord account linked successfully', { domain: '.pretendo.network' }).redirect('/account');
|
||||
});
|
||||
|
||||
router.get('/online-files', async (request, response) => {
|
||||
|
|
@ -529,11 +533,51 @@ router.get('/upgrade', async (request, response) => {
|
|||
return response.redirect('/account/login');
|
||||
}
|
||||
|
||||
// Attempt to get user data
|
||||
let apiResponse = await util.apiGetRequest('/v1/user', {
|
||||
'Authorization': `${request.cookies.token_type} ${request.cookies.access_token}`
|
||||
});
|
||||
|
||||
if (apiResponse.statusCode !== 200) {
|
||||
// Assume expired, refresh and retry request
|
||||
apiResponse = await util.apiPostGetRequest('/v1/login', {}, {
|
||||
refresh_token: request.cookies.refresh_token,
|
||||
grant_type: 'refresh_token'
|
||||
});
|
||||
|
||||
if (apiResponse.statusCode !== 200) {
|
||||
return response.redirect('/account/login');
|
||||
}
|
||||
|
||||
const tokens = apiResponse.body;
|
||||
|
||||
response.cookie('refresh_token', tokens.refresh_token, { domain: '.pretendo.network' });
|
||||
response.cookie('access_token', tokens.access_token, { domain: '.pretendo.network' });
|
||||
response.cookie('token_type', tokens.token_type, { domain: '.pretendo.network' });
|
||||
|
||||
apiResponse = await util.apiGetRequest('/v1/user', {
|
||||
'Authorization': `${tokens.token_type} ${tokens.access_token}`
|
||||
});
|
||||
}
|
||||
|
||||
// If still failed, something went horribly wrong
|
||||
if (apiResponse.statusCode !== 200) {
|
||||
return response.redirect('/account/login');
|
||||
}
|
||||
|
||||
// Set user account info to render data
|
||||
const account = apiResponse.body;
|
||||
const pid = account.pid;
|
||||
|
||||
const pnid = await database.PNID.findOne({ pid });
|
||||
|
||||
const renderData = {
|
||||
layout: 'main',
|
||||
locale: util.getLocale(request.locale.region, request.locale.language),
|
||||
localeString: request.locale.toString(),
|
||||
error: request.cookies.error
|
||||
error: request.cookies.error,
|
||||
currentTier: pnid.get('connections.stripe.price_id'),
|
||||
donationCache: await cache.getStripeDonationCache()
|
||||
};
|
||||
|
||||
const { data: prices } = await stripe.prices.list();
|
||||
|
|
@ -567,7 +611,7 @@ router.get('/upgrade', async (request, response) => {
|
|||
response.render('account/upgrade', renderData);
|
||||
});
|
||||
|
||||
router.post('/checkout/:priceId', async (request, response) => {
|
||||
router.post('/stripe/checkout/:priceId', async (request, response) => {
|
||||
// Verify the user is logged in
|
||||
if (!request.cookies.access_token || !request.cookies.refresh_token || !request.cookies.ph) {
|
||||
return response.redirect('/account/login');
|
||||
|
|
@ -627,16 +671,19 @@ router.post('/checkout/:priceId', async (request, response) => {
|
|||
|
||||
await database.PNID.updateOne({ pid }, {
|
||||
$set: {
|
||||
connections: {
|
||||
stripe: {
|
||||
customer_id: customer.id // ensure PNID always has latest customer ID
|
||||
}
|
||||
}
|
||||
'connections.stripe.customer_id': customer.id // ensure PNID always has latest customer ID
|
||||
}
|
||||
}, { upsert: true }).exec();
|
||||
|
||||
const priceId = request.params.priceId;
|
||||
|
||||
const pnid = await database.PNID.findOne({ pid });
|
||||
|
||||
if (pnid.get('access_level') >= 2) {
|
||||
response.cookie('error', 'Staff members do not need to purchase tiers', { domain: '.pretendo.network' });
|
||||
return response.redirect('/account');
|
||||
}
|
||||
|
||||
try {
|
||||
const session = await stripe.checkout.sessions.create({
|
||||
line_items: [
|
||||
|
|
@ -654,13 +701,90 @@ router.post('/checkout/:priceId', async (request, response) => {
|
|||
return response.redirect(303, session.url);
|
||||
} catch (error) {
|
||||
// Maybe we need a dedicated error page?
|
||||
// O handle this as not cookies?
|
||||
// Or handle this as not cookies?
|
||||
response.cookie('error', error.message, { domain: '.pretendo.network' });
|
||||
return response.redirect('/account');
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/stripe-wh', express.raw({ type: 'application/json' }), async (request, response) => {
|
||||
router.post('/stripe/unsubscribe', async (request, response) => {
|
||||
// Verify the user is logged in
|
||||
if (!request.cookies.access_token || !request.cookies.refresh_token || !request.cookies.ph) {
|
||||
return response.redirect('/account/login');
|
||||
}
|
||||
|
||||
// Attempt to get user data
|
||||
let apiResponse = await util.apiGetRequest('/v1/user', {
|
||||
'Authorization': `${request.cookies.token_type} ${request.cookies.access_token}`
|
||||
});
|
||||
|
||||
if (apiResponse.statusCode !== 200) {
|
||||
// Assume expired, refresh and retry request
|
||||
apiResponse = await util.apiPostGetRequest('/v1/login', {}, {
|
||||
refresh_token: request.cookies.refresh_token,
|
||||
grant_type: 'refresh_token'
|
||||
});
|
||||
|
||||
if (apiResponse.statusCode !== 200) {
|
||||
return response.redirect('/account/login');
|
||||
}
|
||||
|
||||
const tokens = apiResponse.body;
|
||||
|
||||
response.cookie('refresh_token', tokens.refresh_token, { domain: '.pretendo.network' });
|
||||
response.cookie('access_token', tokens.access_token, { domain: '.pretendo.network' });
|
||||
response.cookie('token_type', tokens.token_type, { domain: '.pretendo.network' });
|
||||
|
||||
apiResponse = await util.apiGetRequest('/v1/user', {
|
||||
'Authorization': `${tokens.token_type} ${tokens.access_token}`
|
||||
});
|
||||
}
|
||||
|
||||
// If still failed, something went horribly wrong
|
||||
if (apiResponse.statusCode !== 200) {
|
||||
return response.redirect('/account/login');
|
||||
}
|
||||
|
||||
// Set user account info to render data
|
||||
const account = apiResponse.body;
|
||||
const pid = account.pid;
|
||||
|
||||
const pnid = await database.PNID.findOne({ pid });
|
||||
const subscriptionId = pnid.get('connections.stripe.subscription_id');
|
||||
const tierName = pnid.get('connections.stripe.tier_name');
|
||||
|
||||
if (subscriptionId) {
|
||||
try {
|
||||
await stripe.subscriptions.del(subscriptionId);
|
||||
|
||||
const updateData = {
|
||||
'connections.stripe.subscription_id': null,
|
||||
'connections.stripe.price_id': null,
|
||||
'connections.stripe.tier_level': 0,
|
||||
'connections.stripe.tier_name': null,
|
||||
};
|
||||
|
||||
if (pnid.get('access_level') < 2) {
|
||||
// Fail-safe for if staff members reach here
|
||||
// Mostly only useful during testing
|
||||
updateData.access_level = 0;
|
||||
}
|
||||
|
||||
await database.PNID.updateOne({ pid }, { $set: updateData }).exec();
|
||||
} catch (error) {
|
||||
logger.error(`Error canceling old user subscription | ${pnid.get('connections.stripe.customer_id')}, ${pid}, ${subscriptionId} | - ${error.message}`);
|
||||
|
||||
response.cookie('error', 'Error canceling subscription! Contact support if issue persists', { domain: '.pretendo.network' });
|
||||
|
||||
return response.redirect('/account');
|
||||
}
|
||||
}
|
||||
|
||||
response.cookie('success', `Unsubscribed from ${tierName}`, { domain: '.pretendo.network' });
|
||||
return response.redirect('/account');
|
||||
});
|
||||
|
||||
router.post('/stripe/webhook', express.raw({ type: 'application/json' }), async (request, response) => {
|
||||
const stripeSignature = request.headers['stripe-signature'];
|
||||
let event;
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ const util = require('../util');
|
|||
const { boards } = require('../../boards/boards.json');
|
||||
const router = new Router();
|
||||
|
||||
const { getTrelloCache } = require('../trello');
|
||||
const { getTrelloCache } = require('../cache');
|
||||
|
||||
router.get('/', async (request, response) => {
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ const util = require('../util');
|
|||
const { boards } = require('../../boards/boards.json');
|
||||
const router = new Router();
|
||||
|
||||
const { getTrelloCache } = require('../trello');
|
||||
const { getTrelloCache } = require('../cache');
|
||||
|
||||
router.get('/', async (request, response) => {
|
||||
const renderData = {
|
||||
|
|
|
|||
|
|
@ -11,8 +11,10 @@ const PNIDSchema = new Schema({
|
|||
connections: {
|
||||
stripe: {
|
||||
customer_id: String,
|
||||
subscription_id: String,
|
||||
price_id: String,
|
||||
tier_level: Number,
|
||||
tier_name: String,
|
||||
latest_webhook_timestamp: Number
|
||||
}
|
||||
}
|
||||
|
|
|
|||
139
src/util.js
139
src/util.js
|
|
@ -1,14 +1,13 @@
|
|||
const fs = require('fs-extra');
|
||||
const got = require('got');
|
||||
const crypto = require('crypto');
|
||||
const gmail = require('gmail-send');
|
||||
const Stripe = require('stripe');
|
||||
const mailer = require('./mailer');
|
||||
const database = require('./database');
|
||||
const logger = require('./logger');
|
||||
const config = require('../config.json');
|
||||
|
||||
const stripe = new Stripe(config.stripe.secret_key);
|
||||
const sendGmail = gmail(config.gmail);
|
||||
|
||||
function fullUrl(request) {
|
||||
return `${request.protocol}://${request.hostname}${request.originalUrl}`;
|
||||
|
|
@ -81,27 +80,31 @@ async function handleStripeEvent(event) {
|
|||
const product = await stripe.products.retrieve(subscription.plan.product);
|
||||
const customer = await stripe.customers.retrieve(subscription.customer);
|
||||
|
||||
if (!customer.metadata.pnid_pid && subscription.status !== 'canceled' && subscription.status !== 'unpaid') {
|
||||
// No PNID PID linked to customer. Abort and refund!
|
||||
logger.error(`Stripe user ${customer.id} has no PNID linked! Refunding order`);
|
||||
if (!customer?.metadata?.pnid_pid) {
|
||||
// No PNID PID linked to customer
|
||||
if (subscription.status !== 'canceled' && subscription.status !== 'unpaid') {
|
||||
// Abort and refund!
|
||||
logger.error(`Stripe user ${customer.id} has no PNID linked! Refunding order`);
|
||||
|
||||
await stripe.subscriptions.del(subscription.id);
|
||||
await stripe.subscriptions.del(subscription.id);
|
||||
|
||||
const invoice = await stripe.invoices.retrieve(subscription.latest_invoice);
|
||||
await stripe.refunds.create({
|
||||
payment_intent: invoice.payment_intent
|
||||
});
|
||||
|
||||
try {
|
||||
await sendGmail({
|
||||
to: customer.email,
|
||||
subject: 'Pretendo Subscription Failed - No Linked PNID',
|
||||
text: `Your recent subscription to Pretendo Network has failed.\nThis is due to no PNID PID being linked to the Stripe customer account used. The subscription has been canceled and refunded. Please contact Jon immediately.\nStripe Customer ID: ${customer.id}`
|
||||
const invoice = await stripe.invoices.retrieve(subscription.latest_invoice);
|
||||
await stripe.refunds.create({
|
||||
payment_intent: invoice.payment_intent
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(`Error sending email | ${customer.id}, ${customer.email} | - ${error.message}`);
|
||||
|
||||
try {
|
||||
await mailer.sendMail({
|
||||
to: customer.email,
|
||||
subject: 'Pretendo Network Subscription Failed - No Linked PNID',
|
||||
text: `Your recent subscription to Pretendo Network has failed.\nThis is due to no PNID PID being linked to the Stripe customer account used. The subscription has been canceled and refunded. Please contact Jon immediately.\nStripe Customer ID: ${customer.id}`
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(`Error sending email | ${customer.id}, ${customer.email} | - ${error.message}`);
|
||||
}
|
||||
} else {
|
||||
logger.error(`Stripe user ${customer.id} has no PNID linked!`);
|
||||
}
|
||||
|
||||
|
||||
return;
|
||||
}
|
||||
|
|
@ -128,9 +131,9 @@ async function handleStripeEvent(event) {
|
|||
});
|
||||
|
||||
try {
|
||||
await sendGmail({
|
||||
await mailer.sendMail({
|
||||
to: customer.email,
|
||||
subject: 'Pretendo Subscription Failed - PNID Not Found',
|
||||
subject: 'Pretendo Network Subscription Failed - PNID Not Found',
|
||||
text: `Your recent subscription to Pretendo Network has failed.\nThis is due to the provided PNID not being found. The subscription has been canceled and refunded. Please contact Jon immediately.\nStripe Customer ID: ${customer.id}\nPNID PID: ${pid}`
|
||||
});
|
||||
} catch (error) {
|
||||
|
|
@ -140,14 +143,31 @@ async function handleStripeEvent(event) {
|
|||
return;
|
||||
}
|
||||
|
||||
const updateData = {
|
||||
connections: {
|
||||
stripe: {
|
||||
price_id: subscription.plan.id,
|
||||
tier_level: Number(product.metadata.tier_level || 0),
|
||||
latest_webhook_timestamp: event.created
|
||||
const currentSubscriptionId = pnid.get('connections.stripe.subscription_id');
|
||||
|
||||
if (subscription.status === 'canceled' && currentSubscriptionId && subscription.id !== currentSubscriptionId) {
|
||||
// Canceling old subscription, do nothing but update webhook date
|
||||
|
||||
const updateData = {
|
||||
'connections.stripe.latest_webhook_timestamp': event.created
|
||||
};
|
||||
|
||||
await database.PNID.updateOne({
|
||||
pid,
|
||||
'connections.stripe.latest_webhook_timestamp': {
|
||||
$lte: event.created
|
||||
}
|
||||
}
|
||||
}, { $set: updateData }).exec();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const updateData = {
|
||||
'connections.stripe.subscription_id': subscription.status === 'active' ? subscription.id : null,
|
||||
'connections.stripe.price_id': subscription.status === 'active' ? subscription.plan.id : null,
|
||||
'connections.stripe.tier_level': subscription.status === 'active' ? Number(product.metadata.tier_level || 0) : 0,
|
||||
'connections.stripe.tier_name': subscription.status === 'active' ? product.name : null,
|
||||
'connections.stripe.latest_webhook_timestamp': event.created,
|
||||
};
|
||||
|
||||
if (product.metadata.beta === 'true') {
|
||||
|
|
@ -168,8 +188,69 @@ async function handleStripeEvent(event) {
|
|||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
await database.PNID.updateOne({ pid }, { $set: updateData }, { upsert: true }).exec();
|
||||
await database.PNID.updateOne({
|
||||
pid,
|
||||
'connections.stripe.latest_webhook_timestamp': {
|
||||
$lte: event.created
|
||||
}
|
||||
}, { $set: updateData }).exec();
|
||||
|
||||
if (subscription.status === 'active') {
|
||||
// Get all the customers active subscriptions
|
||||
const { data: activeSubscriptions } = await stripe.subscriptions.list({
|
||||
limit: 100,
|
||||
status: 'active',
|
||||
customer: customer.id
|
||||
});
|
||||
|
||||
// Order subscriptions by creation time and remove the latest one
|
||||
const orderedActiveSubscriptions = activeSubscriptions.sort((a, b) => b.created - a.created);
|
||||
const pastSubscriptions = orderedActiveSubscriptions.slice(1);
|
||||
|
||||
// Remove any old past subscriptions that might still be hanging around
|
||||
for (const pastSubscription of pastSubscriptions) {
|
||||
try {
|
||||
await stripe.subscriptions.del(pastSubscription.id);
|
||||
} catch (error) {
|
||||
logger.error(`Error canceling old user subscription | ${customer.id}, ${pid}, ${pastSubscription.id} | - ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await mailer.sendMail({
|
||||
to: customer.email,
|
||||
subject: 'Pretendo Network Subscription - Active',
|
||||
text: `Thank you for purchasing the ${product.name} tier! We greatly value your support, thank you for helping keep Pretendo Network alive!\nIt may take a moment for your account dashboard to reflect these changes. Please wait a moment and refresh the dashboard to see them!`
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(`Error sending email | ${customer.id}, ${customer.email}, ${pid} | - ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (subscription.status === 'canceled') {
|
||||
try {
|
||||
await mailer.sendMail({
|
||||
to: customer.email,
|
||||
subject: 'Pretendo Network Subscription - Canceled',
|
||||
text: `Your subscription for the ${product.name} tier has been canceled. We thank for your previous support, and hope you still enjoy the network! `
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(`Error sending email | ${customer.id}, ${customer.email}, ${pid} | - ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (subscription.status === 'unpaid') {
|
||||
try {
|
||||
await mailer.sendMail({
|
||||
to: customer.email,
|
||||
subject: 'Pretendo Network Subscription - Unpaid',
|
||||
text: `Your subscription for the ${product.name} tier has been canceled due to non payment. We thank for your previous support, and hope you still enjoy the network! `
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(`Error sending email | ${customer.id}, ${customer.email}, ${pid} | - ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,11 +10,22 @@
|
|||
<img src="{{account.mii.image_url}}" class="mii" />
|
||||
<p class="miiname">{{account.mii.name}}</p>
|
||||
<p class="username" value="{{account.username}}">PNID: {{account.username}}</p>
|
||||
{{#if tierName}}
|
||||
<p class="tier-name tier-level-{{tierLevel}}" value="{{tierName}}">{{tierName}}</p>
|
||||
{{else}}
|
||||
<p class="tier-name tier-level-0" value="Default">Default</p>
|
||||
{{/if}}
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<a class="button secondary" id="download-cemu-files" href="/account/online-files" download>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-download"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>
|
||||
<p class="download-caption">Download account files</p>
|
||||
<p class="caption">Download account files</p>
|
||||
<p class="cemu-warning">(will not work on Nintendo Network)</p>
|
||||
</a>
|
||||
|
||||
<a class="button secondary" id="account-upgrade" href="/account/upgrade">
|
||||
<p class="caption">Upgrade account</p>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -52,9 +63,8 @@
|
|||
</div>
|
||||
|
||||
<div class="setting-card">
|
||||
<h2 class="header">Beta access</h2>
|
||||
{{#if isTester}}
|
||||
<fieldset>
|
||||
<h2 class="header">Server environment</h2>
|
||||
<fieldset {{#unless isTester}}disabled{{/unless}}>
|
||||
<form class="server-selection" id="server">
|
||||
<input type="radio" id="prod" name="server_selection" value="prod" checked="{{ eq account.server_access_level 'prod'}}">
|
||||
<label for="prod">
|
||||
|
|
@ -68,6 +78,7 @@
|
|||
</svg>
|
||||
<h2>Production</h2>
|
||||
</label>
|
||||
{{#if isTester}}
|
||||
<input type="radio" id="beta" name="server_selection" value="beta" checked="{{ neq account.server_access_level 'prod'}}">
|
||||
<label for="beta">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
|
|
@ -75,17 +86,14 @@
|
|||
</svg>
|
||||
<h2>Beta</h2>
|
||||
</label>
|
||||
{{/if}}
|
||||
</form>
|
||||
</fieldset>
|
||||
{{#if discordUser}}
|
||||
<p>Connected to Discord as {{ discordUser.username }}#{{ discordUser.discriminator }}</p>
|
||||
<button class="button secondary" id="remove-discord-connection">Remove Discord account</button>
|
||||
{{else}}
|
||||
<p>To gain access to the developer chats, run /command in the server.</p>
|
||||
{{/if}}
|
||||
{{#unless isTester}}
|
||||
<p>Beta servers are exclusive to beta testers.<br>To become a beta tester, upgrade to a higher account tier.</p>
|
||||
{{else}}
|
||||
<p>Beta servers are exclusive to testers. To upgrade to a higher tier, click <a href="/account/upgrade">here</a>.</p>
|
||||
{{/if}}
|
||||
<p>Your current tier (insert current tier{{currentTier}}) gives you beta server access. Cool!</p>
|
||||
{{/unless}}
|
||||
</div>
|
||||
|
||||
<h2 class="section-header" id="security">Sign in and security</h2>
|
||||
|
|
@ -125,7 +133,17 @@
|
|||
</div>
|
||||
|
||||
<h2 class="section-header" id="other">Other settings</h2>
|
||||
<div class="setting-card span-both-columns">
|
||||
<div class="setting-card">
|
||||
<h2 class="header">Discord</h2>
|
||||
{{#if discordUser}}
|
||||
<p>Connected to Discord as {{ discordUser.username }}#{{ discordUser.discriminator }}</p>
|
||||
<button class="button secondary" id="remove-discord-connection">Remove Discord account</button>
|
||||
{{else}}
|
||||
<p>To link your Discord account, run /whatever in the <a href="https://invite.gg/pretendo" >Discord server</a></p>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
<div class="setting-card">
|
||||
<h2 class="header">Newsletter</h2>
|
||||
<form id="other">
|
||||
<input type="checkbox" id="marketing" name="marketing" {{#if account.flags.marketing}}checked{{/if}}>
|
||||
|
|
@ -138,10 +156,10 @@
|
|||
</div>
|
||||
|
||||
|
||||
{{#if linked}}
|
||||
{{#if success}}
|
||||
<div class="banner-notice success">
|
||||
<div>
|
||||
<p>{{ linked }} account linked successfully</p>
|
||||
<p>{{success}}</p>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
|
@ -149,27 +167,9 @@
|
|||
{{#if error}}
|
||||
<div class="banner-notice error">
|
||||
<div>
|
||||
<p>{{ error }}</p>
|
||||
<p>{{error}}</p>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if stripe.showNotice}}
|
||||
{{#if stripe.success}}
|
||||
<div class="banner-notice success">
|
||||
<div>
|
||||
<p>Account upgraded successfully</p>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{#if stripe.error}}
|
||||
<div class="banner-notice error">
|
||||
<div>
|
||||
<p>Account upgrade failed</p>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
<script src="/assets/js/account.js"></script>
|
||||
|
|
@ -1,6 +1,11 @@
|
|||
<link rel="stylesheet" href="/assets/css/upgrade.css" />
|
||||
|
||||
<div class="wrapper">
|
||||
|
||||
<a href="/account" class="back-arrow">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-arrow-left"><line x1="19" y1="12" x2="5" y2="12"></line><polyline points="12 19 5 12 12 5"></polyline></svg>
|
||||
<span>Back</span>
|
||||
</a>
|
||||
|
||||
<div class="account-form-wrapper">
|
||||
<a class="logotype" href="/">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="120" height="39.876">
|
||||
|
|
@ -26,60 +31,65 @@
|
|||
</g>
|
||||
</svg>
|
||||
</a>
|
||||
|
||||
|
||||
<h1 class="title dot">Upgrade</h1>
|
||||
<p class="caption">Subscribing to a tier gives you access to our developer chats and, depending on the tier,
|
||||
to our beta servers. Please someone reword this, it sucks. No fr.</p>
|
||||
|
||||
|
||||
<form method="post">
|
||||
{{#each tiers}}
|
||||
<form method="post" data-current-tier="{{currentTier}}">
|
||||
{{#each tiers}}
|
||||
<input type="radio" class="tier-radio" data-tier-name="{{this.name}}" name="tier" value="{{this.price_id}}" id="{{this.price_id}}" />
|
||||
<label class="tier" for="{{this.price_id}}">
|
||||
<label class="tier" for="{{this.price_id}}">
|
||||
<div class="tier-thumbnail">
|
||||
<img src="{{this.thumbnail}}" width="100%" height="auto" alt="Tier icon" />
|
||||
<img src="{{this.thumbnail}}" width="100%" height="auto" alt="Tier icon" />
|
||||
</div>
|
||||
<div class="tier-text">
|
||||
<div class="tier-text">
|
||||
<p class="tier-name">{{this.name}}</p>
|
||||
<div class="tier-perks">
|
||||
{{#each this.perks}}
|
||||
|
||||
<div>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-plus"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>
|
||||
<p>{{this}}</p>
|
||||
<p>{{this}}</p>
|
||||
</div>
|
||||
{{/each}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="price">
|
||||
<span>{{this.price}}</span> / month
|
||||
</p>
|
||||
</label>
|
||||
{{/each}}
|
||||
</label>
|
||||
{{/each}}
|
||||
|
||||
<div class="button-wrapper">
|
||||
<button type="submit" id="submitButton">
|
||||
Select a tier
|
||||
</button>
|
||||
<button class="disabled" type="submit" id="submitButton">Select a tier</button>
|
||||
<button class="unsubscribe hidden" id="unsubModalShowButton">Unsubscribe</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="unsub-modal-wrapper hidden">
|
||||
<div class="unsub-modal">
|
||||
<h1 class="title dot">Unsubscribe</h1>
|
||||
<p class="unsub-modal-caption">Are you sure you want to unsubscribe from <span>tiername</span>?
|
||||
You will lose access to the perks associated with that tier.</p>
|
||||
<div class="unsub-modal-button-wrapper">
|
||||
<button class="cancel" id="unsubModalCloseButton">Cancel</button>
|
||||
<button class="confirm" id="unsubModalConfirmButton">Unsubscribe</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="switch-tier-modal-wrapper hidden">
|
||||
<div class="switch-tier-modal">
|
||||
<h1 class="title dot">Change tier</h1>
|
||||
<p class="switch-tier-modal-caption">Are you sure you want to unsubscribe
|
||||
from <span class="oldtier">oldtiername</span> and subscribe to <span class="newtier">newtiername</span>?</p>
|
||||
<div class="switch-tier-modal-button-wrapper">
|
||||
<button class="cancel" id="switchTierCloseButton">Cancel</button>
|
||||
<button class="confirm" id="switchTierConfirmButton">Confirm</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.querySelector('#submitButton').addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const form = document.querySelector('form');
|
||||
const selectedTier = form.querySelector('input[type="radio"]:checked').value;
|
||||
form.action = `/account/checkout/${selectedTier}`;
|
||||
form.submit();
|
||||
});
|
||||
|
||||
document.querySelector('form').addEventListener('change', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const clickedTierID = e.srcElement.id;
|
||||
const clickedTierName = e.srcElement.dataset.tierName;
|
||||
document.querySelector('#submitButton').innerText = `Subscribe to ${clickedTierName}`;
|
||||
});
|
||||
</script>
|
||||
<script src="/assets/js/upgrade.js" />
|
||||
Loading…
Reference in New Issue
Block a user