mirror of
https://github.com/monkeytypegame/monkeytype.git
synced 2024-09-20 07:16:17 +08:00
add finalizeCheckout
This commit is contained in:
parent
2b2fedc128
commit
64af76859c
|
@ -32,8 +32,19 @@ const dummyUser = {
|
|||
jest.spyOn(AuthUtils, "verifyIdToken").mockResolvedValue(mockDecodedToken);
|
||||
|
||||
const stripePriceMock = jest.spyOn(Stripe, "getPrices");
|
||||
const stripeCreateCheckout = jest.spyOn(Stripe, "createCheckout");
|
||||
const stripeCreateCheckoutMock = jest.spyOn(Stripe, "createCheckout");
|
||||
const stripeGetCheckoutMock = jest.spyOn(Stripe, "getCheckout");
|
||||
const stripeGetSubscriptionMock = jest.spyOn(Stripe, "getSubscription");
|
||||
|
||||
const userGetUserMock = jest.spyOn(UserDal, "getUser");
|
||||
const userLinkCustomerByUidMock = jest.spyOn(
|
||||
UserDal,
|
||||
"linkStripeCustomerIdByUid"
|
||||
);
|
||||
const userUpdatePremiumMock = jest.spyOn(
|
||||
UserDal,
|
||||
"updatePremiumByStripeCustomerId"
|
||||
);
|
||||
|
||||
const mockApp = request(app);
|
||||
const configuration = Configuration.getCachedConfiguration();
|
||||
|
@ -44,8 +55,8 @@ describe("store controller test", () => {
|
|||
await enablePremiumFeatures(true);
|
||||
});
|
||||
afterEach(async () => {
|
||||
[stripePriceMock, stripeCreateCheckout, userGetUserMock].forEach((it) =>
|
||||
it.mockReset()
|
||||
[stripePriceMock, stripeCreateCheckoutMock, userGetUserMock].forEach(
|
||||
(it) => it.mockReset()
|
||||
);
|
||||
});
|
||||
it("should create checkout for single subscription for first time user", async () => {
|
||||
|
@ -53,7 +64,7 @@ describe("store controller test", () => {
|
|||
stripePriceMock.mockResolvedValue([
|
||||
{ id: "price_id", type: "recurring" },
|
||||
]);
|
||||
stripeCreateCheckout.mockResolvedValue("http://example.com");
|
||||
stripeCreateCheckoutMock.mockResolvedValue("http://example.com");
|
||||
userGetUserMock.mockResolvedValue(dummyUser);
|
||||
|
||||
//WHEN
|
||||
|
@ -71,7 +82,7 @@ describe("store controller test", () => {
|
|||
expect(checkoutData).toHaveProperty("redirectUrl", "http://example.com");
|
||||
|
||||
expect(stripePriceMock).toHaveBeenCalledWith(["prime_monthly"]);
|
||||
expect(stripeCreateCheckout).toHaveBeenCalledWith({
|
||||
expect(stripeCreateCheckoutMock).toHaveBeenCalledWith({
|
||||
line_items: [{ price: "price_id", quantity: 1 }],
|
||||
billing_address_collection: "auto",
|
||||
success_url:
|
||||
|
@ -85,7 +96,7 @@ describe("store controller test", () => {
|
|||
it("should create checkout for single one_time_payment for first time user", async () => {
|
||||
//GIVEN
|
||||
stripePriceMock.mockResolvedValue([{ id: "price_id", type: "one_time" }]);
|
||||
stripeCreateCheckout.mockResolvedValue("http://example.com");
|
||||
stripeCreateCheckoutMock.mockResolvedValue("http://example.com");
|
||||
userGetUserMock.mockResolvedValue(dummyUser);
|
||||
|
||||
//WHEN
|
||||
|
@ -103,7 +114,7 @@ describe("store controller test", () => {
|
|||
expect(checkoutData).toHaveProperty("redirectUrl", "http://example.com");
|
||||
|
||||
expect(stripePriceMock).toHaveBeenCalledWith(["prime_monthly"]);
|
||||
expect(stripeCreateCheckout).toHaveBeenCalledWith({
|
||||
expect(stripeCreateCheckoutMock).toHaveBeenCalledWith({
|
||||
line_items: [{ price: "price_id", quantity: 1 }],
|
||||
billing_address_collection: "auto",
|
||||
success_url:
|
||||
|
@ -120,7 +131,7 @@ describe("store controller test", () => {
|
|||
stripePriceMock.mockResolvedValue([
|
||||
{ id: "price_id", type: "recurring" },
|
||||
]);
|
||||
stripeCreateCheckout.mockResolvedValue("http://example.com");
|
||||
stripeCreateCheckoutMock.mockResolvedValue("http://example.com");
|
||||
const returningUser = _.merge(dummyUser, {
|
||||
stripeData: { customerId: "cust_1234" },
|
||||
});
|
||||
|
@ -141,7 +152,7 @@ describe("store controller test", () => {
|
|||
expect(checkoutData).toHaveProperty("redirectUrl", "http://example.com");
|
||||
|
||||
expect(stripePriceMock).toHaveBeenCalledWith(["prime_monthly"]);
|
||||
expect(stripeCreateCheckout).toHaveBeenCalledWith({
|
||||
expect(stripeCreateCheckoutMock).toHaveBeenCalledWith({
|
||||
line_items: [{ price: "price_id", quantity: 1 }],
|
||||
billing_address_collection: "auto",
|
||||
success_url:
|
||||
|
@ -250,6 +261,121 @@ describe("store controller test", () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
describe("finalizeCheckout", () => {
|
||||
beforeEach(async () => {
|
||||
await enablePremiumFeatures(true);
|
||||
userLinkCustomerByUidMock.mockResolvedValue();
|
||||
userUpdatePremiumMock.mockResolvedValue();
|
||||
});
|
||||
afterEach(async () => {
|
||||
[
|
||||
userLinkCustomerByUidMock,
|
||||
userUpdatePremiumMock,
|
||||
stripeGetCheckoutMock,
|
||||
stripeGetSubscriptionMock,
|
||||
].forEach((it) => it.mockReset());
|
||||
});
|
||||
|
||||
it("should update premium for subscriptions", async () => {
|
||||
//GIVEN
|
||||
stripeGetCheckoutMock.mockResolvedValue({
|
||||
client_reference_id: uid,
|
||||
customer: "customerId",
|
||||
payment_status: "paid",
|
||||
mode: "subscription",
|
||||
subscription: "subscriptionId",
|
||||
} as Stripe.Session);
|
||||
stripeGetSubscriptionMock.mockResolvedValue({
|
||||
status: "active",
|
||||
customer: "customerId",
|
||||
start_date: 10,
|
||||
current_period_end: 20,
|
||||
} as Stripe.Subscription);
|
||||
|
||||
//WHEN
|
||||
await mockApp
|
||||
.post("/store/checkouts/sessionId")
|
||||
.set("Authorization", "Bearer 123456789")
|
||||
.send()
|
||||
.expect(200);
|
||||
|
||||
//THEN
|
||||
expect(stripeGetCheckoutMock).toHaveBeenCalledWith("sessionId");
|
||||
expect(userLinkCustomerByUidMock).toHaveBeenCalledWith(uid, "customerId");
|
||||
expect(stripeGetSubscriptionMock).toHaveBeenCalledWith("subscriptionId");
|
||||
expect(userUpdatePremiumMock).toHaveBeenCalledWith(
|
||||
"customerId",
|
||||
10000,
|
||||
20000
|
||||
);
|
||||
});
|
||||
|
||||
it("should fail for mismatch user", async () => {
|
||||
//the MT user in the stripe session is not the same as in the request
|
||||
//GIVEN
|
||||
stripeGetCheckoutMock.mockResolvedValue({
|
||||
client_reference_id: "anotherUser",
|
||||
} as Stripe.Session);
|
||||
|
||||
//WHEN /THEN
|
||||
await mockApp
|
||||
.post("/store/checkouts/theSessionId")
|
||||
.set("Authorization", "Bearer 123456789")
|
||||
.send()
|
||||
.expect(400)
|
||||
.expect(expectErrorMessage("Invalid checkout for the current user."));
|
||||
});
|
||||
it("should fail for unpaid subscriptions", async () => {
|
||||
//GIVEN
|
||||
stripeGetCheckoutMock.mockResolvedValue({
|
||||
client_reference_id: uid,
|
||||
customer: "customerId",
|
||||
payment_status: "unpaid",
|
||||
} as Stripe.Session);
|
||||
|
||||
//WHEN
|
||||
await mockApp
|
||||
.post("/store/checkouts/sessionId")
|
||||
.set("Authorization", "Bearer 123456789")
|
||||
.send()
|
||||
.expect(500)
|
||||
.expect(expectErrorMessage("Session is not paid."));
|
||||
});
|
||||
it("should fail for non subscriptions", async () => {
|
||||
//GIVEN
|
||||
stripeGetCheckoutMock.mockResolvedValue({
|
||||
client_reference_id: uid,
|
||||
customer: "customerId",
|
||||
payment_status: "paid",
|
||||
mode: "payment",
|
||||
} as Stripe.Session);
|
||||
|
||||
//WHEN
|
||||
await mockApp
|
||||
.post("/store/checkouts/sessionId")
|
||||
.set("Authorization", "Bearer 123456789")
|
||||
.send()
|
||||
.expect(500)
|
||||
.expect(
|
||||
expectErrorMessage("Session mode payment is not supported yet.")
|
||||
);
|
||||
});
|
||||
|
||||
describe("validations", () => {
|
||||
it("should fail if premium feature is disabled", async () => {
|
||||
//GIVEN
|
||||
await enablePremiumFeatures(false);
|
||||
|
||||
//WHEN
|
||||
await mockApp
|
||||
.post("/store/checkouts/theSessionId")
|
||||
.set("Authorization", "Bearer 123456789")
|
||||
.send()
|
||||
.expect(503)
|
||||
.expect(expectErrorMessage("Premium is temporarily disabled."));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
async function enablePremiumFeatures(premium: boolean): Promise<void> {
|
||||
|
|
|
@ -2,6 +2,7 @@ import _ from "lodash";
|
|||
import { ObjectId } from "mongodb";
|
||||
import { updateStreak } from "../../src/dal/user";
|
||||
import * as UserDAL from "../../src/dal/user";
|
||||
import MonkeyError from "../../src/utils/error";
|
||||
|
||||
const mockPersonalBest = {
|
||||
acc: 1,
|
||||
|
@ -781,4 +782,92 @@ describe("UserDal", () => {
|
|||
await expect(streak).toBe(expectedStreak);
|
||||
}
|
||||
});
|
||||
describe("linkStripeCustomerIdByUid", () => {
|
||||
it("should link user by uid", async () => {
|
||||
//GIVEN
|
||||
const uid = new ObjectId().toHexString();
|
||||
const customerId = new ObjectId().toHexString();
|
||||
await UserDAL.addUser("user" + uid, uid + "@example.com", uid);
|
||||
|
||||
//WHEN
|
||||
await UserDAL.linkStripeCustomerIdByUid(uid, customerId);
|
||||
|
||||
//THEN
|
||||
const readUser = await UserDAL.getUser(uid, "test");
|
||||
expect(readUser).toHaveProperty("stripeData.customerId", customerId);
|
||||
});
|
||||
it("should link user by email", async () => {
|
||||
//GIVEN
|
||||
const uid = new ObjectId().toHexString();
|
||||
const email = uid + "@example.com";
|
||||
const customerId = new ObjectId().toHexString();
|
||||
await UserDAL.addUser("user" + uid, email, uid);
|
||||
|
||||
//WHEN
|
||||
await UserDAL.linkStripeCustomerIdByEmail(email, customerId);
|
||||
|
||||
//THEN
|
||||
const readUser = await UserDAL.getUser(uid, "test");
|
||||
expect(readUser).toHaveProperty("stripeData.customerId", customerId);
|
||||
});
|
||||
it("should link user by uid if already linked", async () => {
|
||||
//GIVEN
|
||||
const uid = new ObjectId().toHexString();
|
||||
const customerId = new ObjectId().toHexString();
|
||||
await UserDAL.addUser("user" + uid, uid + "@example.com", uid);
|
||||
await UserDAL.linkStripeCustomerIdByUid(uid, customerId);
|
||||
|
||||
//WHEN
|
||||
await UserDAL.linkStripeCustomerIdByUid(uid, customerId);
|
||||
|
||||
//THEN
|
||||
const readUser = await UserDAL.getUser(uid, "test");
|
||||
expect(readUser).toHaveProperty("stripeData.customerId", customerId);
|
||||
});
|
||||
|
||||
it("should fail linking user by uid if already linked with a different customerId", async () => {
|
||||
//GIVEN
|
||||
const uid = new ObjectId().toHexString();
|
||||
const customerId = new ObjectId().toHexString();
|
||||
await UserDAL.addUser("user" + uid, uid + "@example.com", uid);
|
||||
await UserDAL.linkStripeCustomerIdByUid(uid, "diffenentCustomerId");
|
||||
|
||||
//WHEN / THEN
|
||||
await expect(
|
||||
UserDAL.linkStripeCustomerIdByUid(uid, customerId)
|
||||
).rejects.toThrow(new MonkeyError(404, "Cannot link customer to user."));
|
||||
});
|
||||
it("should fail for unknown uid", async () => {
|
||||
await expect(
|
||||
UserDAL.linkStripeCustomerIdByUid("unknownUid", "customerId")
|
||||
).rejects.toThrow(new MonkeyError(404, "Cannot link customer to user."));
|
||||
});
|
||||
it("should fail for unknown email", async () => {
|
||||
await expect(
|
||||
UserDAL.linkStripeCustomerIdByEmail("unknown@example.com", "customerId")
|
||||
).rejects.toThrow(new MonkeyError(404, "Cannot link customer to user."));
|
||||
});
|
||||
});
|
||||
describe("updatePremiumByStripeCustomerId", () => {
|
||||
it("should set premium by customerId", async () => {
|
||||
//GIVEN
|
||||
const uid = new ObjectId().toHexString();
|
||||
const customerId = new ObjectId().toHexString();
|
||||
await UserDAL.addUser("user" + uid, uid + "@example.com", uid);
|
||||
await UserDAL.linkStripeCustomerIdByUid(uid, customerId);
|
||||
|
||||
//WHEN
|
||||
await UserDAL.updatePremiumByStripeCustomerId(customerId, 10, 20);
|
||||
|
||||
//THEN
|
||||
const readUser = await UserDAL.getUser(uid, "test");
|
||||
expect(readUser).toHaveProperty("premium.startTimestamp", 10);
|
||||
expect(readUser).toHaveProperty("premium.expirationTimestamp", 20);
|
||||
});
|
||||
it("should fail for unknown customerId", async () => {
|
||||
await expect(
|
||||
UserDAL.updatePremiumByStripeCustomerId("unknownCustomerId", 10, 20)
|
||||
).rejects.toThrow(new MonkeyError(404, "Cannot update premium info."));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { MonkeyResponse } from "../../utils/monkey-response";
|
||||
import * as UserDal from "../../dal/user";
|
||||
|
||||
import * as Stripe from "../../services/stripe";
|
||||
import MonkeyError from "../../utils/error";
|
||||
import { getFrontendUrl } from "../../utils/misc";
|
||||
|
@ -50,3 +49,58 @@ export async function createCheckout(
|
|||
|
||||
return new MonkeyResponse("Checkout created", { redirectUrl });
|
||||
}
|
||||
|
||||
export async function finalizeCheckout(
|
||||
req: MonkeyTypes.Request
|
||||
): Promise<MonkeyResponse> {
|
||||
const { uid } = req.ctx.decodedToken;
|
||||
const stripeSessionId = req.params.stripeSessionId;
|
||||
const session = await Stripe.getCheckout(stripeSessionId);
|
||||
|
||||
//check the checkout was for the current user
|
||||
if (session.client_reference_id !== uid) {
|
||||
throw new MonkeyError(400, "Invalid checkout for the current user.");
|
||||
}
|
||||
|
||||
//session must be linked to a stripe customer
|
||||
await UserDal.linkStripeCustomerIdByUid(uid, session.customer as string);
|
||||
|
||||
//check session payment is not pending
|
||||
if (
|
||||
session.payment_status !== "paid" &&
|
||||
session.payment_status !== "no_payment_required"
|
||||
) {
|
||||
throw new MonkeyError(500, "Session is not paid.");
|
||||
}
|
||||
|
||||
switch (session.mode) {
|
||||
case "subscription":
|
||||
await processSubscription(session.subscription as string);
|
||||
break;
|
||||
default:
|
||||
throw new MonkeyError(
|
||||
500,
|
||||
`Session mode ${session.mode} is not supported yet.`
|
||||
);
|
||||
}
|
||||
|
||||
return new MonkeyResponse("Checkout finalized", {});
|
||||
}
|
||||
|
||||
async function processSubscription(subscriptionId: string): Promise<void> {
|
||||
const subscription = await Stripe.getSubscription(subscriptionId);
|
||||
|
||||
if (subscription.status === "active") {
|
||||
//
|
||||
const startDate = subscription.start_date * 1000;
|
||||
const endDate = subscription.current_period_end * 1000;
|
||||
|
||||
await UserDal.updatePremiumByStripeCustomerId(
|
||||
subscription.customer as string,
|
||||
startDate,
|
||||
endDate
|
||||
);
|
||||
} else {
|
||||
//we don't need to handle other states as premium validity is calculated based on the expirationTimestamp.
|
||||
}
|
||||
}
|
||||
|
|
|
@ -312,6 +312,7 @@ function getRelevantUserInfo(
|
|||
"_id",
|
||||
"lastResultHashes",
|
||||
"note",
|
||||
"stripeData",
|
||||
]);
|
||||
}
|
||||
|
||||
|
|
|
@ -10,14 +10,16 @@ import { authenticateRequest } from "../../middlewares/auth";
|
|||
|
||||
const router = Router();
|
||||
|
||||
const validateFeatureEnabled = validateConfiguration({
|
||||
criteria: (configuration) => {
|
||||
return configuration.users.premium.enabled;
|
||||
},
|
||||
invalidMessage: "Premium is temporarily disabled.",
|
||||
});
|
||||
|
||||
router.post(
|
||||
"/checkouts",
|
||||
validateConfiguration({
|
||||
criteria: (configuration) => {
|
||||
return configuration.users.premium.enabled;
|
||||
},
|
||||
invalidMessage: "Premium is temporarily disabled.",
|
||||
}),
|
||||
validateFeatureEnabled,
|
||||
authenticateRequest(),
|
||||
validateRequest({
|
||||
body: {
|
||||
|
@ -35,5 +37,16 @@ router.post(
|
|||
}),
|
||||
asyncHandler(StoreController.createCheckout)
|
||||
);
|
||||
router.post(
|
||||
"/checkouts/:stripeSessionId",
|
||||
validateFeatureEnabled,
|
||||
authenticateRequest(),
|
||||
validateRequest({
|
||||
params: {
|
||||
stripeSessionId: joi.string().required(),
|
||||
},
|
||||
}),
|
||||
asyncHandler(StoreController.finalizeCheckout)
|
||||
);
|
||||
|
||||
export default router;
|
||||
|
|
|
@ -1047,3 +1047,58 @@ export async function checkIfUserIsPremium(uid: string): Promise<boolean> {
|
|||
if (expirationDate === -1) return true; //lifetime
|
||||
return expirationDate > Date.now();
|
||||
}
|
||||
|
||||
export async function linkStripeCustomerIdByUid(
|
||||
uid: string,
|
||||
customerId: string
|
||||
): Promise<void> {
|
||||
return linkStripeCustomer(customerId, uid, undefined);
|
||||
}
|
||||
export async function linkStripeCustomerIdByEmail(
|
||||
email: string,
|
||||
customerId: string
|
||||
): Promise<void> {
|
||||
return linkStripeCustomer(customerId, undefined, email);
|
||||
}
|
||||
async function linkStripeCustomer(
|
||||
customerId: string,
|
||||
uid?: string,
|
||||
email?: string
|
||||
): Promise<void> {
|
||||
const customerFilter = {
|
||||
$or: [
|
||||
{ "stripeData.customerId": null },
|
||||
{ "stripeData.customerId": customerId },
|
||||
],
|
||||
};
|
||||
const userFilter = uid !== undefined ? { uid } : { email };
|
||||
const filter = {
|
||||
$and: [userFilter, customerFilter],
|
||||
};
|
||||
|
||||
const result = await getUsersCollection().updateOne(filter, {
|
||||
$set: { stripeData: { customerId: customerId } },
|
||||
});
|
||||
|
||||
if (result.matchedCount !== 1) {
|
||||
throw new MonkeyError(404, "Cannot link customer to user.");
|
||||
}
|
||||
}
|
||||
export async function updatePremiumByStripeCustomerId(
|
||||
customerId: string,
|
||||
startDate: number,
|
||||
endDate: number
|
||||
): Promise<void> {
|
||||
const result = await getUsersCollection().updateOne(
|
||||
{ "stripeData.customerId": customerId },
|
||||
{
|
||||
$set: {
|
||||
premium: { startTimestamp: startDate, expirationTimestamp: endDate },
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (result.matchedCount !== 1) {
|
||||
throw new MonkeyError(404, "Cannot update premium info.");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,8 @@ export type Price = {
|
|||
type: Stripe.Price.Type;
|
||||
};
|
||||
export type SessionCreateParams = Stripe.Checkout.SessionCreateParams;
|
||||
export type Session = Stripe.Checkout.Session;
|
||||
export type Subscription = Stripe.Subscription;
|
||||
|
||||
export async function getPrices(
|
||||
lookupKeys: Array<string>
|
||||
|
@ -28,3 +30,15 @@ export async function createCheckout(
|
|||
}
|
||||
return result.url;
|
||||
}
|
||||
|
||||
export async function getCheckout(sessionId: string): Promise<Session> {
|
||||
const session = await stripe.checkout.sessions.retrieve(sessionId);
|
||||
|
||||
return session;
|
||||
}
|
||||
export async function getSubscription(
|
||||
subscriptionId: string
|
||||
): Promise<Subscription> {
|
||||
const subscription = await stripe.subscriptions.retrieve(subscriptionId);
|
||||
return subscription;
|
||||
}
|
||||
|
|
|
@ -11,4 +11,11 @@ export default class Store {
|
|||
|
||||
return await this.httpClient.post(`${BASE_PATH}/checkouts`, { payload });
|
||||
}
|
||||
|
||||
async finalizeCheckout(sessionId: string): Ape.EndpointResponse {
|
||||
return await this.httpClient.post(
|
||||
`${BASE_PATH}/checkouts/${sessionId}`,
|
||||
{}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,9 +15,6 @@ async function fill(): Promise<void> {
|
|||
if (!user) return;
|
||||
|
||||
const data = DB.getSnapshot();
|
||||
|
||||
console.log("++++ fill", { user, data });
|
||||
|
||||
if (!data) return;
|
||||
|
||||
//TODO check backend config for user.premium.enabled
|
||||
|
@ -27,16 +24,22 @@ async function fill(): Promise<void> {
|
|||
const action = urlParams.get("action");
|
||||
|
||||
if (action === "success") {
|
||||
const sessionId = urlParams.get("session_id");
|
||||
alert("complete purchase for sessionId " + sessionId);
|
||||
//TODO: call backend POST /store/checkouts/${sessionId}
|
||||
//TODO: on success reload user info
|
||||
//simulate
|
||||
data.isPremium = true;
|
||||
data.premium = {
|
||||
startTimestamp: 1701471599,
|
||||
expirationTimestamp: 1704149999,
|
||||
};
|
||||
const sessionId = urlParams.get("session_id") || ""; //error handling?
|
||||
const response = await Ape.store.finalizeCheckout(sessionId);
|
||||
if (response.status >= 300) {
|
||||
alert("request failed: " + response.status + " " + response.message);
|
||||
return;
|
||||
}
|
||||
|
||||
const userData = await Ape.users.getData();
|
||||
//error handling?
|
||||
data.premium = userData.data.premium;
|
||||
data.isPremium = userData.data.isPremium;
|
||||
window.location.href =
|
||||
window.location.protocol +
|
||||
"//" +
|
||||
window.location.host +
|
||||
window.location.pathname;
|
||||
} else if (action === "cancel") {
|
||||
alert("purchase cancelled.");
|
||||
}
|
||||
|
@ -49,7 +52,7 @@ async function fill(): Promise<void> {
|
|||
$("#premium_sub_cancel").attr("disabled", "disabled");
|
||||
} else {
|
||||
premiumEndDate = new Date(
|
||||
data.premium?.expirationTimestamp * 1000
|
||||
data.premium?.expirationTimestamp
|
||||
).toDateString();
|
||||
}
|
||||
$("#premium_until").html(premiumEndDate);
|
||||
|
@ -73,6 +76,10 @@ $(".premium_sub").on("click", async (e) => {
|
|||
window.location.href = redirectUrl;
|
||||
});
|
||||
|
||||
$(".premium_sub_cancel").on("click", async (e) => {
|
||||
alert("cancel subscription");
|
||||
});
|
||||
|
||||
export const page = new Page(
|
||||
"store",
|
||||
$(".page.pageStore"),
|
||||
|
|
|
@ -6,7 +6,11 @@
|
|||
<span id="premium_until"></span>
|
||||
.
|
||||
</p>
|
||||
<input type="button" id="premium_sub_cancel" value="Cancel subsciption." />
|
||||
<input
|
||||
type="button"
|
||||
class="premium_sub_cancel"
|
||||
value="Cancel subsciption."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="premiumAvailable hidden">
|
||||
|
|
Loading…
Reference in a new issue