PageSourceSearch

https://power-keys.web.app/javascript/app.js

js power-keys.web.app collected 2026-10-03 12:33:18 UTC 10,357 bytes, 333 lines download raw bytes

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.