From df9b6575bc1fea5a67306a83b546a291963efeda Mon Sep 17 00:00:00 2001 From: Thor Galle Date: Mon, 11 Mar 2024 18:30:08 +0200 Subject: [PATCH 1/7] feat: ignore STP invoices & subscriptions (previously uncommitted but deployed) --- .../stripeEventHandlers/invoiceCreated.js | 8 ++--- .../stripeEventHandlers/invoiceFinalized.js | 8 +++++ .../stripeEventHandlers/invoicePaid.js | 5 +++ .../paymentIntentProcessing.js | 27 +++++++++++---- .../subscriptionDeleted.js | 6 ++++ .../subscriptionUpdated.js | 8 +++++ .../subscriptions/stripeEventHandlers/util.js | 34 +++++++++++++++++++ 7 files changed, 83 insertions(+), 13 deletions(-) create mode 100644 api/src/subscriptions/stripeEventHandlers/util.js diff --git a/api/src/subscriptions/stripeEventHandlers/invoiceCreated.js b/api/src/subscriptions/stripeEventHandlers/invoiceCreated.js index 343c7c12..396c2c16 100644 --- a/api/src/subscriptions/stripeEventHandlers/invoiceCreated.js +++ b/api/src/subscriptions/stripeEventHandlers/invoiceCreated.js @@ -1,12 +1,11 @@ // @ts-check -const functions = require('firebase-functions'); const stripe = require('../stripe'); const getFirebaseUserId = require('../getFirebaseUserId'); const { stripeSubscriptionKeys } = require('../constants'); const removeUndefined = require('../../util/removeUndefined'); const { db } = require('../../firebase'); const { sendSubscriptionRenewalEmail } = require('../../mail'); - +const { isWTMGInvoice } = require('./util'); /** * Handles the `invoice.created` event from Stripe. * Only handles WTMG subscription renewal invoices, ignores other invoices. @@ -20,13 +19,10 @@ module.exports = async (event, res) => { /** @type {import('stripe').Stripe.Invoice} */ const invoice = event.data.object; - const priceIdsObj = functions.config().stripe.price_ids; - const wtmgPriceIds = Object.values(priceIdsObj); - // NOTE: we can only rely on this price ID being accurate because we only look for subscription_cycle invoices // If we were to handle subscription_update invoices here, the price ID may be different, see [one-off-invoice] + const isWtmgSubscriptionInvoice = isWTMGInvoice(invoice); const price = invoice.lines.data[0]?.price; - const isWtmgSubscriptionInvoice = wtmgPriceIds.includes(price?.id || ''); if (invoice.billing_reason !== 'subscription_cycle' || !isWtmgSubscriptionInvoice) { // Ignore invoices that were created for events not related // to WTMG subscription renewals diff --git a/api/src/subscriptions/stripeEventHandlers/invoiceFinalized.js b/api/src/subscriptions/stripeEventHandlers/invoiceFinalized.js index 61222a32..dfdf9ac8 100644 --- a/api/src/subscriptions/stripeEventHandlers/invoiceFinalized.js +++ b/api/src/subscriptions/stripeEventHandlers/invoiceFinalized.js @@ -1,3 +1,7 @@ +// @ts-check + +const { isWTMGInvoice } = require('./util'); + /** * Handles the `invoice.finalized` event from Stripe * @param {*} event @@ -6,6 +10,10 @@ module.exports = async (event, res) => { console.log('Handling invoice.finalized'); const invoice = event.data.object; + if (!isWTMGInvoice(invoice)) { + console.log('Ignoring non-WTMG invoice'); + return res.sendStatus(200); + } // TODO send email with the finalized invoice!? // see "hosted_invoice_url" and "invoice_pdf" diff --git a/api/src/subscriptions/stripeEventHandlers/invoicePaid.js b/api/src/subscriptions/stripeEventHandlers/invoicePaid.js index 6440982c..520797b2 100644 --- a/api/src/subscriptions/stripeEventHandlers/invoicePaid.js +++ b/api/src/subscriptions/stripeEventHandlers/invoicePaid.js @@ -5,6 +5,7 @@ const { } = require('../../mail'); const { stripeSubscriptionKeys } = require('../constants'); const { db } = require('../../firebase'); +const { isWTMGInvoice } = require('./util'); const { latestInvoiceStatusKey, paymentProcessingKey } = stripeSubscriptionKeys; @@ -19,6 +20,10 @@ const { latestInvoiceStatusKey, paymentProcessingKey } = stripeSubscriptionKeys; module.exports = async (event, res) => { console.log('Handling invoice.paid'); const invoice = event.data.object; + if (!isWTMGInvoice(invoice)) { + console.log('Ignoring non-WTMG invoice'); + return res.sendStatus(200); + } const uid = await getFirebaseUserId(invoice.customer); // Set the user's latest invoice state diff --git a/api/src/subscriptions/stripeEventHandlers/paymentIntentProcessing.js b/api/src/subscriptions/stripeEventHandlers/paymentIntentProcessing.js index 0e04c994..56037a21 100644 --- a/api/src/subscriptions/stripeEventHandlers/paymentIntentProcessing.js +++ b/api/src/subscriptions/stripeEventHandlers/paymentIntentProcessing.js @@ -8,13 +8,14 @@ const { const { stripeSubscriptionKeys } = require('../constants'); const { db } = require('../../firebase'); const stripe = require('../stripe'); +const { isWTMGInvoice } = require('./util'); const { latestInvoiceStatusKey, paymentProcessingKey } = stripeSubscriptionKeys; /** * * Inform Firebase of an approved, processing payment + send membership confirmation email - * Special case handlig of SOFORT, where we consider an "network approved" initiated payment + * Special case handling of SOFORT, where we consider an "network approved" initiated payment * already fully closed & settled. * ("first thank you" email, or the "thank you for renewing" email) * @param {import('stripe').Stripe.Event} event @@ -23,12 +24,8 @@ const { latestInvoiceStatusKey, paymentProcessingKey } = stripeSubscriptionKeys; module.exports = async (event, res) => { console.log('Handling payment_intent.processing'); const paymentIntent = /** @type {import('stripe').Stripe.PaymentIntent} */ (event.data.object); - const uid = await getFirebaseUserId(paymentIntent.customer); - - // Set the user's latest invoice state - const privateUserProfileDocRef = db.doc(`users-private/${uid}`); - const privateUserProfileData = (await privateUserProfileDocRef.get()).data(); + // --- Do pre-checks for the usefulness of this event for WTMG --- // Make sure charges are listed for the paymentIntent // @ts-ignore if (!(paymentIntent?.charges?.data instanceof Array)) { @@ -36,7 +33,7 @@ module.exports = async (event, res) => { } // Check if this payment was approved. - // Empirically, charges is part of the event object + // By checking, we've seen that charges is part of the event object const processingCharge = /** @type {import('stripe').Stripe.Charge[]} */ ( // @ts-ignore paymentIntent.charges.data @@ -52,9 +49,17 @@ module.exports = async (event, res) => { return res.sendStatus(200); } + // --- Qualify the event further based on the linked invoice --- // Fetch the related invoice const invoice = await stripe.invoices.retrieve(paymentIntent.invoice); + // Check if the invoice is a WTMG invoice + if (!isWTMGInvoice(invoice)) { + // Ignore invoices that were created for payment events not related to WTMG subscriptions + console.log('Ignoring non-WTMG payment processing event'); + return res.sendStatus(200); + } + // Check if the invoice is related to subscription creation if ( !( @@ -70,6 +75,13 @@ module.exports = async (event, res) => { return res.sendStatus(200); } + // --- Actually process the event --- + + // Get the Firebase user + const uid = await getFirebaseUserId(paymentIntent.customer); + const privateUserProfileDocRef = db.doc(`users-private/${uid}`); + const privateUserProfileData = (await privateUserProfileDocRef.get()).data(); + // Ensure the user is marked as a superfan, with a payment processing indication. const publicUserProfileDocRef = db.doc(`users/${uid}`); const publicUserProfileData = (await publicUserProfileDocRef.get()).data(); @@ -77,6 +89,7 @@ module.exports = async (event, res) => { superfan: true }); await privateUserProfileDocRef.update({ + // Set the user's latest invoice state // Empirically, we know that this status is "open" [latestInvoiceStatusKey]: invoice.status, [paymentProcessingKey]: true diff --git a/api/src/subscriptions/stripeEventHandlers/subscriptionDeleted.js b/api/src/subscriptions/stripeEventHandlers/subscriptionDeleted.js index dc92c682..222c5cb0 100644 --- a/api/src/subscriptions/stripeEventHandlers/subscriptionDeleted.js +++ b/api/src/subscriptions/stripeEventHandlers/subscriptionDeleted.js @@ -4,6 +4,7 @@ const { sendSubscriptionEndedEmail } = require('../../mail'); const removeUndefined = require('../../util/removeUndefined'); const { stripeSubscriptionKeys } = require('../constants'); const getFirebaseUserId = require('../getFirebaseUserId'); +const { isWTMGSubscription } = require('./util'); const { statusKey, cancelAtKey, canceledAtKey } = stripeSubscriptionKeys; @@ -20,6 +21,11 @@ module.exports = async (event, res) => { console.log('Handling customer.subscription.deleted'); /** @type {import('stripe').Stripe.Subscription} */ const subscription = event.data.object; + if (!isWTMGSubscription(subscription)) { + console.log('Ignoring non-WTMG subscription'); + return res.sendStatus(200); + } + const { customer, start_date: startDate, diff --git a/api/src/subscriptions/stripeEventHandlers/subscriptionUpdated.js b/api/src/subscriptions/stripeEventHandlers/subscriptionUpdated.js index 3468b568..3d382a5d 100644 --- a/api/src/subscriptions/stripeEventHandlers/subscriptionUpdated.js +++ b/api/src/subscriptions/stripeEventHandlers/subscriptionUpdated.js @@ -1,7 +1,10 @@ +// @ts-check +// const getFirebaseUserId = require('../getFirebaseUserId'); const { stripeSubscriptionKeys } = require('../constants'); const removeUndefined = require('../../util/removeUndefined'); const { db } = require('../../firebase'); +const { isWTMGSubscription } = require('./util'); const { priceIdKey, @@ -23,6 +26,11 @@ module.exports = async (event, res) => { console.log('Handling subscription.updated'); /** @type {import('stripe').Stripe.Subscription} */ const subscription = event.data.object; + if (!isWTMGSubscription(subscription)) { + console.log('Ignoring non-WTMG subscription'); + return res.sendStatus(200); + } + const uid = await getFirebaseUserId(subscription.customer); // Save updated subscription state in Firebase diff --git a/api/src/subscriptions/stripeEventHandlers/util.js b/api/src/subscriptions/stripeEventHandlers/util.js new file mode 100644 index 00000000..2238976f --- /dev/null +++ b/api/src/subscriptions/stripeEventHandlers/util.js @@ -0,0 +1,34 @@ +// @ts-check +const functions = require('firebase-functions'); + +const priceIdsObj = functions.config().stripe.price_ids; +const wtmgPriceIds = Object.values(priceIdsObj); + +exports.wtmgPriceIds = wtmgPriceIds; + +function isWTMGPriceId(priceId) { + return wtmgPriceIds.includes(priceId); +} +exports.isWTMGPriceId = isWTMGPriceId; + +/** + * @param {import('stripe').Stripe.Invoice} invoice + */ +exports.isWTMGInvoice = function (invoice) { + const priceId = invoice.lines.data[0].price?.id; + if (isWTMGPriceId(priceId)) { + return true; + } + return false; +}; + +/** + * @param {import('stripe').Stripe.Subscription} subscription + */ +exports.isWTMGSubscription = function (subscription) { + const priceId = subscription.items.data[0].price.id; + if (isWTMGPriceId(priceId)) { + return true; + } + return false; +}; From 6c383e883c09c76eb6221caabf01f2c1a54f2795 Mon Sep 17 00:00:00 2001 From: Thor Galle Date: Mon, 11 Mar 2024 18:37:37 +0200 Subject: [PATCH 2/7] fix: handle temporary price IDs on invoices which lead to ignored WTMG events --- .../stripeEventHandlers/invoiceCreated.js | 2 +- .../stripeEventHandlers/invoiceFinalized.js | 2 +- .../stripeEventHandlers/invoicePaid.js | 2 +- .../paymentIntentProcessing.js | 2 +- .../subscriptions/stripeEventHandlers/util.js | 34 +++++++++++++++++-- 5 files changed, 36 insertions(+), 6 deletions(-) diff --git a/api/src/subscriptions/stripeEventHandlers/invoiceCreated.js b/api/src/subscriptions/stripeEventHandlers/invoiceCreated.js index 396c2c16..1ab9d8fa 100644 --- a/api/src/subscriptions/stripeEventHandlers/invoiceCreated.js +++ b/api/src/subscriptions/stripeEventHandlers/invoiceCreated.js @@ -21,7 +21,7 @@ module.exports = async (event, res) => { // NOTE: we can only rely on this price ID being accurate because we only look for subscription_cycle invoices // If we were to handle subscription_update invoices here, the price ID may be different, see [one-off-invoice] - const isWtmgSubscriptionInvoice = isWTMGInvoice(invoice); + const isWtmgSubscriptionInvoice = await isWTMGInvoice(invoice); const price = invoice.lines.data[0]?.price; if (invoice.billing_reason !== 'subscription_cycle' || !isWtmgSubscriptionInvoice) { // Ignore invoices that were created for events not related diff --git a/api/src/subscriptions/stripeEventHandlers/invoiceFinalized.js b/api/src/subscriptions/stripeEventHandlers/invoiceFinalized.js index dfdf9ac8..d7952cce 100644 --- a/api/src/subscriptions/stripeEventHandlers/invoiceFinalized.js +++ b/api/src/subscriptions/stripeEventHandlers/invoiceFinalized.js @@ -10,7 +10,7 @@ const { isWTMGInvoice } = require('./util'); module.exports = async (event, res) => { console.log('Handling invoice.finalized'); const invoice = event.data.object; - if (!isWTMGInvoice(invoice)) { + if (!(await isWTMGInvoice(invoice))) { console.log('Ignoring non-WTMG invoice'); return res.sendStatus(200); } diff --git a/api/src/subscriptions/stripeEventHandlers/invoicePaid.js b/api/src/subscriptions/stripeEventHandlers/invoicePaid.js index 520797b2..673171ad 100644 --- a/api/src/subscriptions/stripeEventHandlers/invoicePaid.js +++ b/api/src/subscriptions/stripeEventHandlers/invoicePaid.js @@ -20,7 +20,7 @@ const { latestInvoiceStatusKey, paymentProcessingKey } = stripeSubscriptionKeys; module.exports = async (event, res) => { console.log('Handling invoice.paid'); const invoice = event.data.object; - if (!isWTMGInvoice(invoice)) { + if (!(await isWTMGInvoice(invoice))) { console.log('Ignoring non-WTMG invoice'); return res.sendStatus(200); } diff --git a/api/src/subscriptions/stripeEventHandlers/paymentIntentProcessing.js b/api/src/subscriptions/stripeEventHandlers/paymentIntentProcessing.js index 56037a21..44f59f45 100644 --- a/api/src/subscriptions/stripeEventHandlers/paymentIntentProcessing.js +++ b/api/src/subscriptions/stripeEventHandlers/paymentIntentProcessing.js @@ -54,7 +54,7 @@ module.exports = async (event, res) => { const invoice = await stripe.invoices.retrieve(paymentIntent.invoice); // Check if the invoice is a WTMG invoice - if (!isWTMGInvoice(invoice)) { + if (!(await isWTMGInvoice(invoice))) { // Ignore invoices that were created for payment events not related to WTMG subscriptions console.log('Ignoring non-WTMG payment processing event'); return res.sendStatus(200); diff --git a/api/src/subscriptions/stripeEventHandlers/util.js b/api/src/subscriptions/stripeEventHandlers/util.js index 2238976f..3ce33789 100644 --- a/api/src/subscriptions/stripeEventHandlers/util.js +++ b/api/src/subscriptions/stripeEventHandlers/util.js @@ -1,5 +1,6 @@ // @ts-check const functions = require('firebase-functions'); +const stripe = require('../stripe'); const priceIdsObj = functions.config().stripe.price_ids; const wtmgPriceIds = Object.values(priceIdsObj); @@ -14,8 +15,37 @@ exports.isWTMGPriceId = isWTMGPriceId; /** * @param {import('stripe').Stripe.Invoice} invoice */ -exports.isWTMGInvoice = function (invoice) { - const priceId = invoice.lines.data[0].price?.id; +exports.isWTMGInvoice = async function (invoice) { + // eslint-disable-next-line camelcase + const { lines, subscription, amount_paid } = invoice; + const lineOne = lines.data[0]; + let price = null; + if (lineOne != null && lineOne.price?.type === 'one_time') { + // In this case, the plan is null, and then this was a modified invoice + // (the customer changed their mind), + // which had a new one-time price & product id generated for it by Stripe + // Get the actual (hopefully!) current product & price from the subscription (?) + // do verify with the amount? + // if price->type is one_time instead of recurring + // TODO: we should consider cancelling & recreating subs on change instead, to avoid confusing issues like this. + // + // The sub will contain the actual price ID of the sub plan. + const sub = await stripe.subscriptions.retrieve(/** @type {string} */ (subscription)); + price = sub.items.data[0].price; + // eslint-disable-next-line camelcase + if (price.unit_amount !== amount_paid) { + // check if this person perhaps updated their sub two times?? :S + // TODO 1: is the price ID info now used in an intergration? ARE THE FAKE ONES + // in our Firebase? That could lead to mega weirdness + // TODO 2: update the product ID too (that one is also unique per invoice) + console.error(`Weirdness occurred in ${sub.id}`); + } + } else { + // Normal case + price = lineOne.price; + } + + const priceId = price?.id; if (isWTMGPriceId(priceId)) { return true; } From 7bde8d42772231d7e6135469986cb8e4f4ed8db8 Mon Sep 17 00:00:00 2001 From: Thor Galle Date: Wed, 13 Mar 2024 11:39:39 +0200 Subject: [PATCH 3/7] fix: load gardens in chunks of 1000 Attempt to fix the gardens not showing up on some devices, which doesn't have a clear cause, but may be due to an issue with the quantity of items requested from Firebase in one call (~2000-5000 now) debug: show debugging information, more chunks, wait between chunks test 100ms interval test 1000ms test 1500ms 1000 chunk size, 500ms chunk size 250, no delay attempt at not converting multiple times between data structures --- src/lib/api/garden.ts | 57 +++++++++++++++---- .../components/Garden/FacilitiesFilter.svelte | 11 +--- src/lib/components/Map/GardenLayer.svelte | 4 +- src/lib/stores/garden.ts | 15 ++++- src/routes/explore/+layout.svelte | 29 ++++++++-- 5 files changed, 88 insertions(+), 28 deletions(-) diff --git a/src/lib/api/garden.ts b/src/lib/api/garden.ts index 29eb3200..55ed166a 100644 --- a/src/lib/api/garden.ts +++ b/src/lib/api/garden.ts @@ -11,13 +11,16 @@ import { getDocFromCache, getDocFromServer, getDoc, - CollectionReference + CollectionReference, + limit, + startAfter } from 'firebase/firestore'; import { getUser } from '$lib/stores/auth'; import { isUploading, uploadProgress, allGardens, isFetchingGardens } from '$lib/stores/garden'; import { db, storage } from './firebase'; import { getDownloadURL, ref, uploadBytesResumable } from 'firebase/storage'; import type { Garden, GardenFacilities } from '$lib/types/Garden'; +import { get } from 'svelte/store'; /** * Get a single garden, if it exists and is listed. Returns `null` otherwise. @@ -27,7 +30,7 @@ export const getGarden = async (id: string) => { const gardenDoc = await getDoc( doc(collection(db(), CAMPSITES) as CollectionReference, id) ); - let data = gardenDoc.data()!; + const data = gardenDoc.data()!; if (gardenDoc.exists() && data.listed) { return { id: gardenDoc.id, @@ -39,17 +42,51 @@ export const getGarden = async (id: string) => { }; export const getAllListedGardens = async () => { + const CHUNK_SIZE = 5000; + // To prevent endless loops in case of unexpected problems or bugs + // Note: this leads to the loop breaking once this number of gardens is reached! + const LOOP_LIMIT_ITEMS = 100000; + isFetchingGardens.set(true); - const q = query(collection(db(), CAMPSITES), where('listed', '==', true)); - const querySnapshot = await getDocs(q); + let startAfterDoc = null; + let iteration = 1; + do { + iteration++; + // Query the chunk of gardens + const q = query.apply(null, [ + collection(db(), CAMPSITES), + where('listed', '==', true), + limit(CHUNK_SIZE), + ...(startAfterDoc ? [startAfter(startAfterDoc)] : []) + ]); + const querySnapshot = await getDocs(q); + + if (querySnapshot.size === CHUNK_SIZE) { + // If a full chunk was fetched, there might be more gardens to fetch + startAfterDoc = querySnapshot.docs[querySnapshot.docs.length - 1]; + } else { + // If the chunk was not full, there are no more gardens to fetch + startAfterDoc = null; + } + + // Merge the map with the existing gardens + allGardens.update((existingGardens) => { + querySnapshot.forEach((doc) => { + const data = doc.data(); + existingGardens.push({ + id: doc.id, + ...data + }); + }); + return existingGardens; + }); + + // Wait 1000ms seconds before fetching the next chunk + // await new Promise((resolve) => setTimeout(() => resolve())); + } while (startAfterDoc != null && iteration < LOOP_LIMIT_ITEMS / CHUNK_SIZE); - const gardens: { [id: string]: Garden } = {}; - querySnapshot.forEach((doc) => { - gardens[doc.id] = { id: doc.id, ...doc.data() }; - }); - allGardens.set(gardens); isFetchingGardens.set(false); - return gardens; + return get(allGardens); }; const doUploadGardenPhoto = async (photo: File, currentUser: User) => { diff --git a/src/lib/components/Garden/FacilitiesFilter.svelte b/src/lib/components/Garden/FacilitiesFilter.svelte index c60ef645..24375cea 100644 --- a/src/lib/components/Garden/FacilitiesFilter.svelte +++ b/src/lib/components/Garden/FacilitiesFilter.svelte @@ -34,14 +34,9 @@ } const returnFilteredGardens = () => { - const gardensFiltered = Object.values($allGardens) + return $allGardens .filter(gardenFilterFacilities, filter.facilities) .filter(gardenFilterCapacity, filter.capacity); - let gardens = {}; - gardensFiltered.map((garden) => { - gardens[garden.id] = { ...garden }; - }); - return gardens; }; let stickToBottom = false; @@ -117,8 +112,8 @@

{@html $_('garden.filter.available', { values: { - amount: Object.values(filteredGardens).length, - styledAmount: `${Object.values(filteredGardens).length}` + amount: filteredGardens.length, + styledAmount: `${filteredGardens.length}` } })}

diff --git a/src/lib/components/Map/GardenLayer.svelte b/src/lib/components/Map/GardenLayer.svelte index 21622162..31c0c796 100644 --- a/src/lib/components/Map/GardenLayer.svelte +++ b/src/lib/components/Map/GardenLayer.svelte @@ -4,7 +4,7 @@ import type maplibregl from 'maplibre-gl'; import type GeoJSON from 'geojson'; - export let allGardens: { [id: string]: Garden }; + export let allGardens: Garden[]; export let selectedGardenId: string | null = null; export let showGardens: boolean; export let showSavedGardens: boolean; @@ -53,7 +53,7 @@ fcSavedGardens.features = []; fcGardensWithoutSavedGardens.features = []; - Object.values(allGardens).map((garden: Garden) => { + allGardens.forEach((garden: Garden) => { if (!garden.id) return; const gardenId = garden.id; diff --git a/src/lib/stores/garden.ts b/src/lib/stores/garden.ts index 4afdf000..581057c1 100644 --- a/src/lib/stores/garden.ts +++ b/src/lib/stores/garden.ts @@ -5,10 +5,11 @@ import type { Garden } from '../types/Garden'; export const isUploading = writable(false); export const uploadProgress = writable(0); export const isFetchingGardens = writable(true); -export const allGardens: Writable<{ [id: string]: Garden }> = writable({}); +export const allGardens: Writable = writable([]); export const addToAllGardens = async (garden: Garden) => { - if (Object.keys(get(allGardens)).length === 0) { + // TODO: length 0 doesn't strictly mean that the gardens are not fetched + if (get(allGardens).length === 0) { isFetchingGardens.set(true); try { await getAllListedGardens(); @@ -18,7 +19,15 @@ export const addToAllGardens = async (garden: Garden) => { } isFetchingGardens.set(false); } else { - allGardens.update((gardens) => ({ ...gardens, [garden.id]: garden })); + allGardens.update((gardens) => { + const index = gardens.findIndex((g) => g.id === garden.id); + if (index) { + gardens[index] = garden; + } else { + gardens.push(garden); + } + return gardens; + }); } }; export const addGardenLocally = (garden: Garden) => addToAllGardens(garden); diff --git a/src/routes/explore/+layout.svelte b/src/routes/explore/+layout.svelte index afa8feb7..7b8e1cae 100644 --- a/src/routes/explore/+layout.svelte +++ b/src/routes/explore/+layout.svelte @@ -33,6 +33,7 @@ import type { Unsubscribe } from 'firebase/firestore'; import { fileDataLayers, removeTrailAnimations } from '$lib/stores/file'; import { isOnIDevicePWA } from '$lib/api/push-registrations'; + import { isEmpty } from 'lodash-es'; let showHiking = false; let showCycling = false; @@ -88,7 +89,9 @@ /** * @type {Garden | null} */ - $: selectedGarden = $isFetchingGardens ? null : $allGardens[$page.params.gardenId] ?? null; + $: selectedGarden = $isFetchingGardens + ? null + : $allGardens.find((g) => g.id === $page.params.gardenId) ?? null; // This variable controls the location of the map. // Don't make it reactive based on its params, so that it can be imperatively controlled. @@ -108,7 +111,7 @@ const selectGarden = (garden) => { const newSelectedId = garden.id; - const newGarden = $allGardens[newSelectedId]; + const newGarden = $allGardens.find((g) => g.id === newSelectedId); setMapToGardenLocation(newGarden); applyZoom = false; // zoom level is not programatically changed when exploring a garden goto(`${routes.MAP}/garden/${newSelectedId}`); @@ -131,16 +134,18 @@ carNoticeShown = false; }; + let fetchError = ''; + // LIFECYCLE HOOKS onMount(async () => { // If the gardens didn't load yet - if (Object.keys($allGardens).length === 0) { + if ($allGardens.length === 0) { // If we're loading the page of a garden, load that one immediately before all other gardens if (isShowingGarden) { const garden = await getGarden($page.params.gardenId); if (garden) { - $allGardens = { [garden.id]: garden }; + $allGardens = [garden]; setMapToGardenLocation(garden); } } @@ -150,6 +155,7 @@ await getAllListedGardens(); } catch (ex) { console.error(ex); + fetchError = 'Error' + ex; isFetchingGardens.set(false); } } @@ -178,7 +184,7 @@ {applyZoom} > - {#if !$isFetchingGardens} + {#if !isEmpty($allGardens)} +
+ {$allGardens.length} gardens + {fetchError} +