Merge pull request #117 from PretendoNetwork/feature-stripe-tiers-change-tier

Added the ability to change tiers
This commit is contained in:
Jonathan Barrow
2022-07-04 12:10:43 -04:00
committed by GitHub
6 changed files with 93 additions and 38 deletions

View File

@@ -31,7 +31,8 @@
border-radius: 100%;
background: var(--btn-secondary);
}
.account-sidebar .user #download-cemu-files {
.account-sidebar .user #download-cemu-files,
.account-sidebar .user #account-upgrade {
display: flex;
flex-flow: column;
align-items: center;
@@ -40,7 +41,8 @@
margin: 24px 0 0;
text-decoration: none;
}
.account-sidebar .user #download-cemu-files p.download-caption {
.account-sidebar .user #download-cemu-files p.caption,
.account-sidebar .user #account-upgrade p.caption {
margin: 15px 0 0;
}
.account-sidebar .user p.cemu-warning {
@@ -152,7 +154,7 @@ fieldset:disabled form label {
color: var(--text);
}
.setting-card #remove-discord-connection {
.setting-card #link-discord-account {
width: 100%;
padding: 12px 48px;
cursor: pointer;

View File

@@ -29,7 +29,7 @@ router.get('/', async (request, response) => {
}
const { upgrade_success } = request.query;
console.log(upgrade_success);
const stripe = {};
if (upgrade_success === 'true') {
stripe.showNotice = true;
@@ -193,6 +193,8 @@ router.get('/', async (request, response) => {
renderData.discordAuthURL = discordAuthURL;
}
renderData.isTester = false;
response.render('account/account', renderData);
});
@@ -616,16 +618,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: [
@@ -643,7 +648,7 @@ 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');
}

View File

@@ -11,6 +11,7 @@ const PNIDSchema = new Schema({
connections: {
stripe: {
customer_id: String,
subscription_id: String,
price_id: String,
tier_level: Number,
latest_webhook_timestamp: Number

View File

@@ -80,7 +80,7 @@ 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') {
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`);
@@ -139,14 +139,30 @@ 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.latest_webhook_timestamp': event.created,
};
if (product.metadata.beta === 'true') {
@@ -167,11 +183,36 @@ 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,
@@ -206,7 +247,6 @@ async function handleStripeEvent(event) {
logger.error(`Error sending email | ${customer.id}, ${customer.email}, ${pid} | - ${error.message}`);
}
}
}
}

View File

@@ -12,9 +12,13 @@
<p class="username" value="{{account.username}}">PNID: {{account.username}}</p>
<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,8 +56,8 @@
</div>
<div class="setting-card">
<h2 class="header">Beta access</h2>
{{#if isTester}}
<h2 class="header">Server Environment</h2>
{{#if isTester}}
<fieldset>
<form class="server-selection" id="server">
<input type="radio" id="prod" name="server_selection" value="prod" checked="{{ eq account.server_access_level 'prod'}}">
@@ -81,10 +85,13 @@
<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>
<a href="{{ discordAuthURL }}">
<button class="button secondary" id="link-discord-account">Link Discord account</button>
</a>
{{/if}}
{{else}}
<p>Beta servers are exclusive to testers. To upgrade to a higher tier, click <a href="/account/upgrade">here</a>.</p>
<p>Account is not a beta tester, locked to production servers.</p>
<p>Beta servers are exclusive to beta testers.<br>To become a beta tester, upgrade to a higher account tier.</p>
{{/if}}
</div>

View File

@@ -26,43 +26,43 @@
</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">
{{#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>
</div>
</form>
</div>
</form>
</div>
</div>
<script>