diff --git a/backend/__tests__/api/controllers/store.spec.ts b/backend/__tests__/api/controllers/store.spec.ts index ab671837e..f562aff01 100644 --- a/backend/__tests__/api/controllers/store.spec.ts +++ b/backend/__tests__/api/controllers/store.spec.ts @@ -75,8 +75,8 @@ describe("store controller test", () => { line_items: [{ price: "price_id", quantity: 1 }], billing_address_collection: "auto", success_url: - "http://localhost:3000/payment/success?session_id={CHECKOUT_SESSION_ID}", - cancel_url: "http://localhost:3000/payment/cancel", + "http://localhost:3000/store?action=success&session_id={CHECKOUT_SESSION_ID}", + cancel_url: "http://localhost:3000/store?action=cancel", client_reference_id: uid, mode: "subscription", customer_email: "test@example.com", @@ -107,8 +107,8 @@ describe("store controller test", () => { line_items: [{ price: "price_id", quantity: 1 }], billing_address_collection: "auto", success_url: - "http://localhost:3000/payment/success?session_id={CHECKOUT_SESSION_ID}", - cancel_url: "http://localhost:3000/payment/cancel", + "http://localhost:3000/store?action=success&session_id={CHECKOUT_SESSION_ID}", + cancel_url: "http://localhost:3000/store?action=cancel", client_reference_id: uid, mode: "payment", customer_creation: "always", @@ -145,8 +145,8 @@ describe("store controller test", () => { line_items: [{ price: "price_id", quantity: 1 }], billing_address_collection: "auto", success_url: - "http://localhost:3000/payment/success?session_id={CHECKOUT_SESSION_ID}", - cancel_url: "http://localhost:3000/payment/cancel", + "http://localhost:3000/store?action=success&session_id={CHECKOUT_SESSION_ID}", + cancel_url: "http://localhost:3000/store?action=cancel", client_reference_id: uid, mode: "subscription", customer: "cust_1234", diff --git a/backend/src/api/controllers/store.ts b/backend/src/api/controllers/store.ts index c43e4d37f..ba07c554e 100644 --- a/backend/src/api/controllers/store.ts +++ b/backend/src/api/controllers/store.ts @@ -27,8 +27,8 @@ export async function createCheckout( }, ], billing_address_collection: "auto", - success_url: `${MY_DOMAIN}/payment/success?session_id={CHECKOUT_SESSION_ID}`, - cancel_url: `${MY_DOMAIN}/payment/cancel`, + success_url: `${MY_DOMAIN}/store?action=success&session_id={CHECKOUT_SESSION_ID}`, + cancel_url: `${MY_DOMAIN}/store?action=cancel`, client_reference_id: uid, }; diff --git a/frontend/src/ts/ape/endpoints/index.ts b/frontend/src/ts/ape/endpoints/index.ts index b6ee0762c..9c3aa46fa 100644 --- a/frontend/src/ts/ape/endpoints/index.ts +++ b/frontend/src/ts/ape/endpoints/index.ts @@ -8,6 +8,7 @@ import Users from "./users"; import ApeKeys from "./ape-keys"; import Public from "./public"; import Configuration from "./configuration"; +import Store from "./store"; export default { Configs, @@ -20,4 +21,5 @@ export default { Users, ApeKeys, Configuration, + Store, }; diff --git a/frontend/src/ts/ape/endpoints/store.ts b/frontend/src/ts/ape/endpoints/store.ts new file mode 100644 index 000000000..f50ec53c2 --- /dev/null +++ b/frontend/src/ts/ape/endpoints/store.ts @@ -0,0 +1,14 @@ +const BASE_PATH = "/store"; +export default class Store { + constructor(private httpClient: Ape.HttpClient) { + this.httpClient = httpClient; + } + + async createCheckout(item: string): Ape.EndpointResponse { + const payload = { + items: [{ lookupKey: item }], + }; + + return await this.httpClient.post(`${BASE_PATH}/checkouts`, { payload }); + } +} diff --git a/frontend/src/ts/ape/index.ts b/frontend/src/ts/ape/index.ts index c456f159b..d22452b33 100644 --- a/frontend/src/ts/ape/index.ts +++ b/frontend/src/ts/ape/index.ts @@ -20,6 +20,7 @@ const Ape = { publicStats: new endpoints.Public(httpClient), apeKeys: new endpoints.ApeKeys(httpClient), configuration: new endpoints.Configuration(httpClient), + store: new endpoints.Store(httpClient), }; export default Ape; diff --git a/frontend/src/ts/controllers/page-controller.ts b/frontend/src/ts/controllers/page-controller.ts index cc5747386..228365505 100644 --- a/frontend/src/ts/controllers/page-controller.ts +++ b/frontend/src/ts/controllers/page-controller.ts @@ -9,6 +9,7 @@ import * as PageLoading from "../pages/loading"; import * as PageProfile from "../pages/profile"; import * as PageProfileSearch from "../pages/profile-search"; import * as Page404 from "../pages/404"; +import * as PageStore from "../pages/store"; import * as PageTransition from "../states/page-transition"; import * as AdController from "../controllers/ad-controller"; import * as Focus from "../test/focus"; @@ -53,6 +54,7 @@ export async function change( login: PageLogin.page, profile: PageProfile.page, profileSearch: PageProfileSearch.page, + store: PageStore.page, 404: Page404.page, }; diff --git a/frontend/src/ts/controllers/route-controller.ts b/frontend/src/ts/controllers/route-controller.ts index d95cc50ee..56c874eac 100644 --- a/frontend/src/ts/controllers/route-controller.ts +++ b/frontend/src/ts/controllers/route-controller.ts @@ -123,6 +123,12 @@ const routes: Route[] = [ }); }, }, + { + path: "/store", + load: (): void => { + PageController.change("store"); + }, + }, ]; export function navigate( diff --git a/frontend/src/ts/db.ts b/frontend/src/ts/db.ts index 3cca5448b..448e648a1 100644 --- a/frontend/src/ts/db.ts +++ b/frontend/src/ts/db.ts @@ -132,6 +132,7 @@ export async function initSnapshot(): Promise< snap.maxStreak = userData?.streak?.maxLength ?? 0; snap.filterPresets = userData.resultFilterPresets ?? []; snap.isPremium = userData?.isPremium; + snap.premium = userData?.premium; const hourOffset = userData?.streak?.hourOffset; snap.streakHourOffset = diff --git a/frontend/src/ts/index.ts b/frontend/src/ts/index.ts index 02656a63a..96cd81ca3 100644 --- a/frontend/src/ts/index.ts +++ b/frontend/src/ts/index.ts @@ -25,6 +25,7 @@ import "./controllers/input-controller"; import "./ready"; import "./controllers/route-controller"; import "./pages/about"; +import "./pages/store"; import "./popups/pb-tables-popup"; import "./elements/scroll-to-top"; import "./popups/mobile-test-config-popup"; diff --git a/frontend/src/ts/pages/store.ts b/frontend/src/ts/pages/store.ts new file mode 100644 index 000000000..b3f9b5dc4 --- /dev/null +++ b/frontend/src/ts/pages/store.ts @@ -0,0 +1,96 @@ +import Page from "./page"; +import * as Skeleton from "../popups/skeleton"; +import { Auth } from "../firebase"; +import * as DB from "../db"; +import Ape from "../ape"; + +function reset(): void { + $(".premiumDisabled").removeClass("hidden"); + $(".premiumActive").addClass("hidden"); + $(".premiumAvailable").addClass("hidden"); +} + +async function fill(): Promise { + const user = Auth?.currentUser; + if (!user) return; + + const data = DB.getSnapshot(); + + console.log("++++ fill", { user, data }); + + if (!data) return; + + //TODO check backend config for user.premium.enabled + $(".premiumDisabled").addClass("hidden"); + + const urlParams = new URLSearchParams(window.location.search); + 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, + }; + } else if (action === "cancel") { + alert("purchase cancelled."); + } + + if (data.isPremium === true) { + let premiumEndDate = ""; + if (data.premium?.expirationTimestamp) { + if (data.premium?.expirationTimestamp === -1) { + premiumEndDate = "the end of the universe"; + $("#premium_sub_cancel").attr("disabled", "disabled"); + } else { + premiumEndDate = new Date( + data.premium?.expirationTimestamp * 1000 + ).toDateString(); + } + $("#premium_until").html(premiumEndDate); + } + + $(".premiumActive").removeClass("hidden"); + } else { + $(".premiumActive").addClass("hidden"); + $(".premiumAvailable").removeClass("hidden"); + } +} + +$(".premium_sub").on("click", async (e) => { + const item = e.currentTarget.getAttribute("data-item") || ""; + const response = await Ape.store.createCheckout(item); + if (response.status >= 300) { + alert("request failed: " + response.status + " " + response.message); + return; + } + const redirectUrl = response.data.redirectUrl; + window.location.href = redirectUrl; +}); + +export const page = new Page( + "store", + $(".page.pageStore"), + "/store", + async () => { + // + }, + async () => { + reset(); + Skeleton.remove("pageStore"); + }, + async () => { + Skeleton.append("pageStore", "main"); + fill(); + }, + async () => { + // + } +); + +Skeleton.save("pageStore"); diff --git a/frontend/src/ts/types/types.d.ts b/frontend/src/ts/types/types.d.ts index 6f6e3edb3..f52144f93 100644 --- a/frontend/src/ts/types/types.d.ts +++ b/frontend/src/ts/types/types.d.ts @@ -14,6 +14,7 @@ declare namespace MonkeyTypes { | "login" | "profile" | "profileSearch" + | "store" | "404"; type Difficulty = "normal" | "expert" | "master"; @@ -608,6 +609,7 @@ declare namespace MonkeyTypes { streakHourOffset?: number; lbOptOut?: boolean; isPremium?: boolean; + premium?: PremiumInfo; } interface UserDetails { @@ -919,4 +921,8 @@ declare namespace MonkeyTypes { histogramDataBucketSize: number; historyStepSize: number; } + interface PremiumInfo { + startTimestamp: number; + expirationTimestamp: number; + } } diff --git a/frontend/static/html/footer.html b/frontend/static/html/footer.html index 3b990b34e..90b293490 100644 --- a/frontend/static/html/footer.html +++ b/frontend/static/html/footer.html @@ -67,6 +67,10 @@
Privacy
+ + +
Store
+
diff --git a/frontend/static/main.html b/frontend/static/main.html index fe9ab2927..bce5c6a37 100644 --- a/frontend/static/main.html +++ b/frontend/static/main.html @@ -39,6 +39,7 @@ compilation.assets["html/pages/account.html"].source() %> <%= compilation.assets["html/pages/profile.html"].source() %> <%= compilation.assets["html/pages/test.html"].source() %> <%= + compilation.assets["html/pages/store.html"].source() %> <%= compilation.assets["html/pages/404.html"].source() %>