Skip to content

Blog

RevenueCat Setup

Use the new Automated Setup Script to create a service account for your app. Make sure the SERVICE_ACCOUNT_NAME is between 6 and 30 characters.

After the script successfully finishes, download the revenuecat-key.json it generates and upload it to RevenueCat.

The script will also output an email for the service account that you will need to invite in the Google Play Console.

Modular vs Namespaced Firestore

There are currently two versions of the Firebase APIs, one that is namespaced and one that is modular. For at least a year, I brushed off the notion that they were different and was repeatedly confused/frustrated when VS Code would lint the Firebase methods I’ve known and loved. Today, I finally decided to educate myself.

Google has a brief explanation about why new apps should use the modular API. Long story short, code organized into modules allows build tools to remove unused code in a process known as “tree shaking”. Code organized using namespaces, does not allow for this type of optimization.

That makes sense.

Unfortunately, the way Google implemented the modular API destroyed the API’s discoverability and made it pretty difficult to get used to. All functions are now isolated from one another and queries must be built from parts instead of chained together.

New Modular API:

const postsQuery = query(
collection(doc(collection(db, 'users'), '123'), 'posts'),
where('status', '==', 'published'),
orderBy('date'),
limit(10)
)

Old Namespaced API:

db.collection('users')
.doc('123')
.collection('posts')
.where('status', '==', 'published')
.orderBy('date')
.limit(10)

In other words, the new syntax is the complete inverse of the original namespaced API. It looks like the namespaced code is being turned inside out. Just look at this line:

collection(doc(collection(db, 'users'), '123'), 'posts'),

Why is the database instance buried three layers deep in the query?

This is a cut and dry case of tree-shaking and bundle size being prioritized over developer ergonomics.

What makes this change even worse is that the NodeJS API continues to use the “namespaced” API. This makes it rather easy to put yourself in the mentally taxing position where half of your stack uses an intuitive Firebase API while the other half reads like something GPT-2 would write. For example, Cloud Functions written in TypeScript use the original namespaced API. Code switching between these two interfaces can be crazy-making…but for now that’s just the way it is.

For this example, I’ll walk through using the Firebase Admin SDK in Firebase Cloud Functions. Cloud functions are considered a “privileged environment” so using the Admin SDK here is generally safe.

You can install the Admin SDK like this:

Terminal window
npm install firebase-admin --save

Next, initialize the SDK:

import * as admin from "firebase-admin";
// Initialize Firebase Admin SDK if not already initialized
if (!admin.apps.length) {
console.log("Initializing Firebase Admin SDK");
admin.initializeApp();
} else {
console.log("Firebase Admin SDK already initialized");
}
export default admin;

This code will run the required initializeApp() method as soon as your Cloud Function is activated. Then, in other files, you can use the admin object by importing it:

import admin from "../firebase_admin";

The Admin SDK gives you access to Firestore, as well as nearly every other Firebase product, via an appropriately named method on the admin instance. To interact with Firestore:

const db = admin.firestore();

The namespaced API let’s you do basically everything by using methods on instances. In the code below, collection(), doc(), and set() are all methods that intuitively chain together to do something that reads like a sentence - “In the “cities” collection, find the “LA” doc and set it’s data:

const data = {
name: 'Los Angeles',
state: 'CA',
country: 'USA'
};
// Add a new document in collection "cities" with ID 'LA'
const res = await db.collection('cities').doc('LA').set(data);

Since the Modular API primarily relates to frontend code where you wouldn’t be using the Admin SDK, this example will use the normal Firebase library.

Install the library like this:

Terminal window
npm install firebase@12.4.0 --save

Once installed, you can initialize your app using the initializeApp function:

import { initializeApp } from "firebase/app";
// TODO: Replace the following with your app's Firebase project configuration
const firebaseConfig = {
FIREBASE_CONFIGURATION
};
// Initialize Firebase
const app = initializeApp(firebaseConfig);

This is where the similarities between the old API and the new one end.

To interact with Firestore, you first need to initialize it and retrieve a reference to the “service”:

// Pass in the initialized app from above
const db = getFirestore(app);

Now, instead of using chained methods like in the namespaced API, you need to use specific functions for action you want to take. For example, addDoc, setDoc, deleteDoc, etc:

import { collection, addDoc } from "firebase/firestore";
try {
const docRef = await addDoc(collection(db, "users"), {
first: "Ada",
last: "Lovelace",
born: 1815
});
console.log("Document written with ID: ", docRef.id);
} catch (e) {
console.error("Error adding document: ", e);
}

In my opinion, this provides a much worse developer experience. Instead of ctrl-spacing your way from one method to the next, you need to remember everything. That sucks.

Copy as Markdown

Since Astro components cannot provide interactivity, you need to create a React component called CopyAsMarkdown.tsx which will handle copying the page content to the user’s clipboard.

This component should accept the page’s title and content as props:

CopyAsMarkdown.tsx
import React, { Component, useState } from "react";
export default function CopyAsMarkdown({
title,
content,
children,
}: {
title: string;
content?: string;
children?: React.ReactNode;
}) {
const [copied, setCopied] = useState(false);
const handleCopy = async () => {
if (!content) return;
try {
await navigator.clipboard.writeText(content);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch (err) {
console.error("Failed to copy content: ", err);
}
};
return (
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
gap: "8px",
}}
>
<h1 id={"_top"}>{title}</h1>
<button
onClick={handleCopy}
disabled={!content}
style={{
padding: "6px 12px",
fontSize: "14px",
border: "1px solid #ccc",
borderRadius: "4px",
backgroundColor: copied ? "#4CAF50" : "#f9f9f9",
color: copied ? "white" : "#333",
cursor: content ? "pointer" : "not-allowed",
opacity: content ? 1 : 0.5,
transition: "all 0.2s ease",
alignItems: "center",
display: "flex",
gap: "6px",
}}
>
{children && children}
{copied ? "Copied!" : "Copy as Markdown"}
</button>
</div>
);
}

Create a new component in src/components to replace the default PageTitle component.

This component will use the React component from above. In order to make the React component interactive, you need to add the client:load directive:

PageTitleWithCopyButton.astro
---
import { Icon } from "@astrojs/starlight/components";
import CopyAsMarkdown from "./CopyAsMarkdown";
---
<CopyAsMarkdown
client:load
title={Astro.locals.starlightRoute.entry.data.title}
content={Astro.locals.starlightRoute.entry.body}
>
<Icon name="document" />
</CopyAsMarkdown>
<style>
@layer starlight.core {
h1 {
margin-top: 1rem;
font-size: var(--sl-text-h1);
line-height: var(--sl-line-height-headings);
font-weight: 600;
color: var(--sl-color-white);
}
}
</style>

In your astro.config.mjs file, override the default PageTitle component with the new component you just created:

// @ts-check
import { defineConfig } from "astro/config";
import starlight from "@astrojs/starlight";
import react from "@astrojs/react";
// https://astro.build/config
export default defineConfig({
integrations: [
starlight({
components: {
PageTitle: "./src/components/PageTitleWithCopyButton.astro",
},
}),
react(),
],
});

Flutter, Firebase, and Google Sign In 7.1.0

In typical Google fashion, the google_sign_in package was updated recently and if you missed the migration guide, your apps are broken. In this post, I’ll walk you through using version 7.1.0 of the google_sign_in package with Firebase and Flutter.

To get started, add the newest google_sign_in version to your pubspec.yaml:

google_sign_in: ^7.1.0

If you don’t have one already, create a new Firebase project in the Firebase console and then navigate to the authentication tab. Under “Sign-in method”, select “Add new provider” and choose “Google”.

You can create a web client ID a few ways.

Once you have the client ID, add it to your environment variables file. I typically create a config.json file in my assets folder and load my variables from that using String.fromEnvironment:

config.json
{
"SERVER_CLIENT_ID": "YOUR_CLIENT_ID"
}
await GoogleSignIn.instance.initialize(
serverClientId: const String.fromEnvironment("SERVER_CLIENT_ID"));

You will need an iOS client ID to sign in with Google on iOS. To get one of these:

  1. Log into your Google Cloud Console
  2. Navigate to APIs & Services
  3. Select “Create credentials”
  4. Select OAuth Client ID
  5. Choose iOS from the application type dropdown
  6. Fill in each of the fields and click save

Once you have the client ID, copy it and add it to your Info.plist file.

https://firebase.google.com/docs/auth/flutter/federated-auth

Developer console is not set up correctly.

Section titled “Developer console is not set up correctly.”

GoogleSignInException(code GoogleSignInExceptionCode.unknownError, [28444] Developer console is not set up correctly., null)

As pointed out in the docs, “[clientId] is the identifier for your client application, as provided by the Google Sign In server configuration”. In other words, you need to set the serverClientId to a web client ID on Android, not an android client ID.

Failed to sign in with Google: GoogleSignInException(code GoogleSignInExceptionCode.clientConfigurationError, serverClientId must be provided on Android, null)

Ensure that you are initializing the GoogleSignIn instance before calling any other methods.

await GoogleSignIn.instance
.initialize(serverClientId: const String.fromEnvironment("ANDROID_CLIENT_ID"));

Execution failed for task ‘:app:checkDebugDuplicateClasses’.

Section titled “Execution failed for task ‘:app:checkDebugDuplicateClasses’.”

Execution failed for task ‘:app:checkDebugDuplicateClasses’. Querying the mapped value of provider(java.util.Set) before task ‘:app:processDebugGoogleServices’ has completed is not supported

Update the Kotlin version in android/gradle/wrapper/gradle-wrapper.properties and android/settings.gradle to the latest Kotlin version (ex. 8.10):

distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
settings.gradle
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0" // apply true
id "com.android.application" version "8.3.0" apply false
// START: FlutterFire Configuration
id "com.google.gms.google-services" version "4.3.14" apply false
id "com.google.firebase.crashlytics" version "2.8.1" apply false
// END: FlutterFire Configuration
id "org.jetbrains.kotlin.android" version "1.8.10" apply false
}