1const prices = {}; 2let currentUser = null; 3 4const firebaseConfig = { 5 apiKey: "AIzaSyDn9jkdtJY3_4eUt5ZAcHqjtJXClH8y0uQ", 6 authDomain: "power-keys.firebaseapp.com", 7 projectId: "power-keys", 8 storageBucket: "power-keys.appspot.com", 9 messagingSenderId: "320796259059", 10 appId: "1:320796259059:web:cc0481d737a34e4705fd86", 11 measurementId: "G-5GX2FDT7GM", 12}; 13 14// Replace with your cloud functions location 15const functionLocation = "us-east4"; 16 17// Initialize Firebase 18const firebaseApp = firebase.initializeApp(firebaseConfig); 19const db = firebaseApp.firestore(); 20 21/** 22 * Helper: copy label text into placeholders for all FirebaseUI inputs. 23 */ 24function applyFirebaseUiPlaceholders() { 25 const fields = document.querySelectorAll( 26 "#firebaseui-auth-container .mdl-textfield" 27 ); 28 29 fields.forEach((field) => { 30 // Avoid re-applying to the same field over and over 31 if (field.dataset.pkPlaceholderApplied === "true") return; 32 33 const input = field.querySelector("input"); 34 const label = field.querySelector(".mdl-textfield__label"); 35 if (!input || !label) return; 36 37 // Use label text as placeholder (strip trailing * if present) 38 const labelText = label.textContent.replace(/\s*\*$/, "").trim(); 39 if (!labelText) return; 40 41 input.placeholder = labelText; 42 field.dataset.pkPlaceholderApplied = "true"; 43 }); 44} 45 46/** 47 * Firebase Authentication configuration 48 */ 49const firebaseUI = new firebaseui.auth.AuthUI(firebase.auth()); 50 51const firebaseUiConfig = { 52 callbacks: { 53 signInSuccessWithAuthResult: function () { 54 // let redirect happen 55 return true; 56 }, 57 uiShown: function () { 58 // Hide loader once FirebaseUI is rendered 59 const loader = document.querySelector("#loader"); 60 if (loader) loader.style.display = "none"; 61 62 // Set custom title text 63 const titleEl = document.querySelector( 64 "#firebaseui-auth-container .firebaseui-title" 65 ); 66 if (titleEl) { 67 titleEl.textContent = "Sign in or sign up"; 68 } 69 70 // Apply placeholders for the current screen 71 applyFirebaseUiPlaceholders(); 72 73 // Observe DOM changes so password / reset / confirm screens also get placeholders 74 const container = document.getElementById("firebaseui-auth-container"); 75 if (container && !container._pkObserverAttached) { 76 const observer = new MutationObserver(() => { 77 applyFirebaseUiPlaceholders(); 78 }); 79 observer.observe(container, { childList: true, subtree: true }); 80 container._pkObserverAttached = true; 81 } 82 }, 83 }, 84 signInFlow: "popup", 85 signInSuccessUrl: "/", 86 signInOptions: [firebase.auth.EmailAuthProvider.PROVIDER_ID], 87 credentialHelper: firebaseui.auth.CredentialHelper.NONE, 88}; 89 90/** 91 * Auth state listener 92 */ 93firebase.auth().onAuthStateChanged((firebaseUser) => { 94 if (firebaseUser) { 95 // Signed in 96 const loader = document.querySelector("#loader"); 97 if (loader) loader.style.display = "none"; 98 99 const authContainer = document.querySelector("#firebaseui-auth-container"); 100 const mainEl = document.querySelector("main"); 101 if (authContainer) authContainer.style.display = "none"; 102 if (mainEl) mainEl.style.display = "block"; 103 104 currentUser = firebaseUser.uid; 105 startDataListeners(); 106 } else { 107 // Signed out 108 const authContainer = document.querySelector("#firebaseui-auth-container"); 109 const mainEl = document.querySelector("main"); 110 if (authContainer) authContainer.style.display = "block"; 111 if (mainEl) mainEl.style.display = "none"; 112 113 firebaseUI.start("#firebaseui-auth-container", firebaseUiConfig); 114 } 115}); 116 117/** 118 * Data listeners 119 */ 120function startDataListeners() { 121 // Build the plan buttons from Firestore prices 122 const plansContainer = document.getElementById("plans"); 123 if (plansContainer) { 124 plansContainer.innerHTML = ""; 125 } 126 127 db.collection("products") 128 .where("active", "==", true) 129 .get() 130 .then(async (querySnapshot) => { 131 if (querySnapshot.empty) { 132 console.warn("No active products found in Firestore."); 133 return; 134 } 135 136 // For now, we assume a single product (Power Keys)
137 const productDoc = querySnapshot.docs[0]; 138 139 const priceSnap = await productDoc.ref 140 .collection("prices") 141 .where("active", "==", true) 142 .orderBy("unit_amount") 143 .get(); 144 145 let annual = null; 146 let monthly = null; 147 148 priceSnap.docs.forEach((priceDoc) => { 149 const priceId = priceDoc.id; 150 const data = priceDoc.data(); 151 prices[priceId] = data; 152 153 const interval = data.recurring?.interval || data.interval; 154 const currency = data.currency || "usd"; 155 const unitAmount = data.unit_amount || 0; 156 157 const formatted = new Intl.NumberFormat("en-US", { 158 style: "currency", 159 currency, 160 }).format((unitAmount / 100).toFixed(2)); 161 162 if (interval === "year" || interval === "annual") { 163 annual = { id: priceId, label: formatted }; 164 } else if (interval === "month" || interval === "monthly") { 165 monthly = { id: priceId, label: formatted }; 166 } 167 }); 168 169 // Create the two plan buttons 170 if (!plansContainer) return; 171 172 if (annual) { 173 const btnAnnual = document.createElement("button"); 174 btnAnnual.type = "button"; 175 btnAnnual.className = "plan-button plan-button-primary"; 176 btnAnnual.textContent = `Subscribe to Annual - ${annual.label}`; 177 btnAnnual.addEventListener("click", () => startCheckout(annual.id)); 178 plansContainer.appendChild(btnAnnual); 179 } 180 181 if (monthly) { 182 const btnMonthly = document.createElement("button"); 183 btnMonthly.type = "button"; 184 btnMonthly.className = "plan-button plan-button-secondary"; 185 btnMonthly.textContent = `Subscribe to Monthly - ${monthly.label}`; 186 btnMonthly.addEventListener("click", () => startCheckout(monthly.id)); 187 plansContainer.appendChild(btnMonthly); 188 } 189 }); 190 191 // Get all subscriptions for the customer 192 db.collection("users") 193 .doc(currentUser) 194 .collection("subscriptions") 195 .orderBy("current_period_end", "desc") 196 .limit(1) 197 .onSnapshot(async (snapshot) => { 198 const subscribeEl = document.querySelector("#subscribe"); 199 const accountEl = document.querySelector("#my-subscription"); 200 const linkAppEl = document.querySelector("#link-app"); 201 202 if (snapshot.empty) { 203 // No subscription â show Subscribe screen 204 if (subscribeEl) subscribeEl.style.display = "block"; 205 if (accountEl) accountEl.style.display = "none"; 206 if (linkAppEl) linkAppEl.style.display = "none"; 207 return; 208 } 209 210 // One subscription 211 const subscription = snapshot.docs[0].data(); 212 const isActive = subscription.status === "active"; 213 const isPastDue = subscription.status === "past_due"; 214 215 if (subscribeEl) { 216 subscribeEl.style.display = isActive || isPastDue ? "none" : "block"; 217 } 218 if (accountEl) { 219 accountEl.style.display = isActive || isPastDue ? "block" : "none"; 220 } 221 if (linkAppEl) { 222 linkAppEl.style.display = isActive ? "block" : "none"; 223 } 224 }); 225} 226 227/** 228 * Event listeners 229 */ 230 231// Signout buttons (both views) 232const signoutMain = document.getElementById("signout"); 233if (signoutMain) { 234 signoutMain.addEventListener("click", () => firebase.auth().signOut()); 235} 236 237const signoutAccount = document.getElementById("signout-account"); 238if (signoutAccount) { 239 signoutAccount.addEventListener("click", () => firebase.auth().signOut()); 240} 241 242// Link button (deep link to app) 243const linkAppBtn = document.getElementById("link-app"); 244if (linkAppBtn) { 245 linkAppBtn.addEventListener("click", async () => { 246 const getCustomToken = firebase 247 .app() 248 .functions("us-east4") 249 .httpsCallable("getCustomToken"); 250 const { data } = await getCustomToken(); // TODO: handle failure 251 const linkURL = "powerkeys:auth?token=" + data.token; 252 window.location.assign(linkURL); 253 }); 254} 255 256/** 257 * Start a Stripe checkout session for the given priceId 258 */ 259async function startCheckout(priceId) { 260 document.querySelectorAll("button").forEach((b) => (b.disabled = true)); 261 262 const selectedPrice = { price: priceId }; 263 264 // For prices with metered billing we need to omit the quantity parameter. 265 // For all other prices we set quantity to 1. 266 if (prices[priceId]?.recurring?.usage_type !== "metered") { 267 selectedPrice.quantity = 1; 268 } 269 270 const checkoutSession = { 271 automatic_tax: true, 272 tax_id_collection: true, 273 collect_shipping_address: true, 274 allow_promotion_codes: true, 275 line_items: [selectedPrice], 276 success_url: window.location.origin, 277 cancel_url: window.location.origin, 278 }; 279 280 // For one time payments set mode to payment. 281 if (prices[priceId]?.type === "one_time") { 282 checkoutSession.mode = "payment"; 283 checkoutSession.payment_method_types = ["card", "sepa_debit", "sofort"]; 284 } 285 286 const docRef = await db 287 .collection("users") 288 .doc(currentUser) 289 .collection("checkout_sessions") 290 .add(checkoutSession); 291 292 // Wait for the CheckoutSession to get attached by the extension 293 docRef.onSnapshot((snap) => { 294 const data = snap.data(); 295 if (!data) return; 296 297 const { error, url } = data; 298 if (error) { 299 alert(`An error occurred: ${error.message}`); 300 document.querySelectorAll("button").forEach((b) => (b.disabled = false)); 301 } 302 if (url) { 303 window.location.assign(url); 304 } 305 }); 306} 307 308// Billing portal handler 309document 310 .querySelector("#billing-portal-button") 311 ?.addEventListener("click", async () => { 312 document.querySelectorAll("button").forEach((b) => (b.disabled = true)); 313 314 // Call billing portal function
315 const functionRef = firebase 316 .app() 317 .functions(functionLocation) 318 .httpsCallable( 319 "ext-firestore-stripe-payments-createPortalLink" 320 ); 321 const { data } = await functionRef({ 322 returnUrl: window.location.origin, 323 }); 324 window.location.assign(data.url); 325 }); 326 327// Get custom claim role helper (unchanged, kept in case you use it) 328async function getCustomClaimRole() { 329 await firebase.auth().currentUser.getIdToken(true); 330 const decodedToken = 331 await firebase.auth().currentUser.getIdTokenResult(); 332 return decodedToken.claims.stripeRole; 333}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.