public/js/account.js (298 lines of code) (raw):

/* * Copyright 2022 Spotify AB * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ class AccountController { constructor(subscriptionsRoot, spotifyLinkRoot, model) { this.model = model; this.view = new AccountView( subscriptionsRoot, spotifyLinkRoot, model, this.handleTierCtaClicked.bind(this), this.handleFullUnsubscribeClick.bind(this), this.handleSpotifyConnectionClicked.bind(this) ); this.handleTierCtaClicked.bind(this); this.loadSubscriptionStatus().then(() => this.view.render()); } async loadSubscriptionStatus() { const res = await fetch("/api/user-spotify-entitlements"); if (res.status !== 200) { this.model.isLinked = false; this.model.syncSubscriptions(); } else { const data = await res.json(); this.model.isLinked = true; this.model.setSubscriptions(data.entitlements); } } async handleTierCtaClicked(tier) { const isSubscribed = this.model.isSubscribedTo(tier); if (isSubscribed) { await this.unsubscribeFromShow(tier); } else { await this.subscribeToShow(tier); } this.view.render(); } async handleFullUnsubscribeClick() { await this.unsubscribeFromAllShows(); this.view.render(); } async handleSpotifyConnectionClicked() { const isLinked = this.model.isLinked; if (isLinked) { await this.unlinkFromSpotify(); this.view.render(); } else { this.connectToSpotify(); } } async subscribeToShow(show) { if (this.model.isLinked) { await fetch("/api/user-spotify-add-entitlements", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ entitlements: [show] }), }); } else { await fetch("/api/update-subscription", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ entitlements: Array.from( new Set([...this.model.subscriptions, show]) ), }), }); } this.model.syncSubscriptions(); } async unsubscribeFromShow(show) { /* The endpoint delete-entitlements requires a list of the entitlements(s) that should be removed from the user. For detailed information: https://developer.spotify.com/documentation/open-access/reference/#/operations/delete-entitlements */ if (this.model.isLinked) { await fetch("/api/user-spotify-delete-entitlements", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ entitlements: [show] }), }); } else { await fetch("/api/update-subscription", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ entitlements: this.model.subscriptions.filter((s) => s !== show), }), }); } this.model.syncSubscriptions(); } async unsubscribeFromAllShows() { /* When calling the endpoint replace-entitlements with an empty list, all the user's entitlements will be removed. For detailed information: https://developer.spotify.com/documentation/open-access/reference/#/operations/replace-entitlements */ if (this.model.isLinked) { await fetch("/api/user-spotify-replace-entitlements", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ entitlements: [] }), }); } else { await fetch("/api/update-subscription", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ entitlements: [], }), }); } this.model.syncSubscriptions(); } connectToSpotify() { window.location.replace("/api/entrypoint"); } unlinkFromSpotify() { return fetch("/api/user-spotify-unlink", { method: "POST", headers: { "Content-Type": "application/json" }, }).then(() => (this.model.isLinked = false)); } } class AccountModel { constructor() { this.isLinked = false; this.subscriptions = []; } setSubscriptions(subscriptions) { this.subscriptions = subscriptions; } syncSubscriptions() { const savedEntitlements = Cookies.get("entitlements"); this.subscriptions = savedEntitlements ? JSON.parse(savedEntitlements) : []; } isSubscribedTo(tier) { return this.subscriptions.includes(tier); } } class AccountView { constructor( subscriptionsRoot, spotifyLinkRoot, model, onTierCtaClicked, onFullUnsubscribeClick, onSpotifyConnectionClicked ) { this.subscriptionsRoot = subscriptionsRoot; this.spotifyLinkRoot = spotifyLinkRoot; this.model = model; this.onTierCtaClicked = onTierCtaClicked; this.onFullUnsubscribeClick = onFullUnsubscribeClick; this.onSpotifyConnectionClicked = onSpotifyConnectionClicked; } render() { this.renderSubscriptions(); this.renderSpotifyLink(); } renderSpotifyLink() { this.spotifyLinkRoot.innerText = ""; const isLinked = this.model.isLinked; const unlinkInfo = document.createElement("p"); if (isLinked) { unlinkInfo.textContent = "Your account is already connected to Spotify. To unlink your account click the button below, this will remove your access to your paid podcasts on the Spotify app."; } else { unlinkInfo.textContent = "By linking your account to Spotify, you'll get access to any of your paid podcasts on the Spotify app."; } this.spotifyLinkRoot.appendChild(unlinkInfo); const unlinkButton = document.createElement("button"); unlinkButton.classList.add("spotify-link"); unlinkButton.addEventListener("click", this.onSpotifyConnectionClicked); this.spotifyLinkRoot.appendChild(unlinkButton); const spotifyLogo = document.createElement("img"); spotifyLogo.src = "/assets/img/Spotify_Icon_RGB_White.png"; spotifyLogo.width = 20; spotifyLogo.height = 20; unlinkButton.appendChild(spotifyLogo); const buttonText = document.createElement("span"); if (isLinked) { buttonText.textContent = "Unlink from Spotify"; } else { buttonText.textContent = "Connect to Spotify"; } unlinkButton.appendChild(buttonText); } renderSubscriptions() { const premiumTierNode = this.renderTier( "Living on the Moon", "Access to all episodes from Living on the Moon.", 3, "premium-tier-subscribers", "https://open.spotify.com/show/0EwATaqqn7Yb0LX6O9XiqI", "/assets/img/living-on-the-moon-artwork.jpg", "Podcast artwork of the show Living on the Moon" ); const bonusTierNode = this.renderTier( "The Dark Side", "Includes all bonus content from The Dark Side.", 2, "bonus-tier-subscribers", "https://open.spotify.com/show/0hFhphwy0gCuuFkOGF4BRu", "/assets/img/the-dark-side-artwork.jpg", "Podcast artwork of the show called The Dark Side" ); const noSubscriptionsIndicator = this.renderNoSubscriptionsIndicator(); const fullUnsubscribeButton = this.renderFullUnsubscribeButton(); this.subscriptionsRoot.innerText = ""; this.subscriptionsRoot.appendChild(noSubscriptionsIndicator); this.subscriptionsRoot.appendChild(premiumTierNode); this.subscriptionsRoot.appendChild(bonusTierNode); this.subscriptionsRoot.appendChild(fullUnsubscribeButton); } renderTier(title, description, price, tier, showUrl, coverUrl, coverAlt) { const isSubscribed = this.model.isSubscribedTo(tier); const container = document.createElement("div"); container.classList.add("subscription-tier"); if (isSubscribed) { container.classList.add("subscribed"); } const showInfo = document.createElement("div"); showInfo.classList.add("show-info"); container.appendChild(showInfo); const subscribedIndicator = document.createElement("div"); subscribedIndicator.classList.add("subscribed-indicator"); subscribedIndicator.textContent = "Subscribed"; if (isSubscribed) { subscribedIndicator.classList.add("show"); } showInfo.appendChild(subscribedIndicator); const tierTitle = document.createElement("h3"); tierTitle.textContent = title; showInfo.appendChild(tierTitle); const tierDescription = document.createElement("p"); tierDescription.textContent = description; tierDescription.classList.add("subscription-description"); showInfo.appendChild(tierDescription); const tierPrice = document.createElement("p"); let tierPriceTextContent = `\$${price} per month.`; if (isSubscribed) { const now = new Date(); // prettier-ignore const monthToPresentation = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"]; const renewalDateString = ` Renews ${now.getDate()} ${ monthToPresentation[now.getMonth()] } ${now.getFullYear() + 1}.`; tierPriceTextContent += renewalDateString; } tierPrice.textContent = tierPriceTextContent; tierPrice.classList.add("subscription-price"); showInfo.appendChild(tierPrice); const tierCtaButton = document.createElement("button"); tierCtaButton.textContent = isSubscribed ? "Unsubscribe" : "Subscribe"; tierCtaButton.classList.add("subscription-button"); tierCtaButton.addEventListener("click", () => this.onTierCtaClicked(tier)); showInfo.appendChild(tierCtaButton); const showCover = document.createElement("div"); showCover.classList.add("show-cover"); container.appendChild(showCover); const showLink = document.createElement("a"); showLink.href = showUrl; showLink.target = "_blank"; showCover.appendChild(showLink); const showCoverImage = document.createElement("img"); showCoverImage.src = coverUrl; showCoverImage.alt = coverAlt; showLink.appendChild(showCoverImage); return container; } renderNoSubscriptionsIndicator() { const noSubscriptionsIndicator = document.createElement("p"); noSubscriptionsIndicator.textContent = "You have no active subscriptions"; noSubscriptionsIndicator.id = "no-subscriptions"; if (this.model.subscriptions.length === 0) { noSubscriptionsIndicator.classList.add("show"); } return noSubscriptionsIndicator; } renderFullUnsubscribeButton() { const fullUnsubscribeButton = document.createElement("button"); fullUnsubscribeButton.textContent = "Unsubscribe from all podcasts"; fullUnsubscribeButton.classList.add("subscription-button"); fullUnsubscribeButton.id = "full-unsubscribe-button"; if (this.model.subscriptions.length === 2) { fullUnsubscribeButton.classList.add("show"); } fullUnsubscribeButton.addEventListener( "click", this.onFullUnsubscribeClick ); return fullUnsubscribeButton; } } const redirectIfUnauthenticated = () => { const authenticated = Boolean(Cookies.get("is_authenticated")); if (!authenticated) { window.location.href = "/login.html?redirect_to=/my-account.html"; } }; const setUpPage = () => { const subscriptionsRoot = document.querySelector("#subscription-tiers"); const spotifyLinkRoot = document.querySelector("#spotify-link"); const model = new AccountModel(); new AccountController(subscriptionsRoot, spotifyLinkRoot, model); }; redirectIfUnauthenticated(); setUpPage();