Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions content/docs/dashboard/paywalls.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@ You can toggle which paywalls are showing by using the **date toggle**, located

Choose **Custom** to select any arbitrary date range to filter by.

<Note>
{/* <Note>
The date toggle works when you are viewing your paywalls as a
[**list**](#viewing-paywall-metrics).
</Note>
**list**.
</Note> */}

### Viewing paywalls by status

Expand All @@ -48,15 +48,15 @@ Here's what each status means:
| Archived | Paywalls that have been archived. These can be restored if needed. |
| Search | Perform a search across all of the app's paywalls, regardless of its status. |

### Viewing paywalls as a table or list
{/* ### Viewing paywalls as a table or list

To toggle between viewing your paywalls by either a table or list, click the toggle buttons at the top:

<Frame>![](/images/paywalls-overview-toggle-by-metric.png)</Frame>

When viewing them as a **list**, Superwall also displays additional metrics.
When viewing them as a **list**, Superwall also displays additional metrics. */}

### Viewing paywall metrics
{/* ### Viewing paywall metrics

Choose the **list** view to see high-level metrics about each paywall:

Expand All @@ -81,7 +81,7 @@ Each metric displays the data in the time frame that's selected from the date to
<Tip>
Click on any of these values at the top of the list to order the data by that metric, either
ascending or descending.
</Tip>
</Tip> */}

### Creating a new paywall

Expand Down
44 changes: 30 additions & 14 deletions content/docs/integrations/firebase.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ description: "The Firebase integration automatically sends Superwall subscriptio

The Firebase integration automatically sends Superwall subscription and payment events to Firebase Analytics (Google Analytics 4) using the Measurement Protocol. Track subscription lifecycle events, analyze revenue metrics, and leverage Firebase's powerful analytics capabilities with automatic event mapping and ecommerce tracking.

<Warning>
This integration requires you to set the Firebase App Instance ID using `setIntegrationAttributes` in your client app. Without it, events will be silently skipped and won't appear in Firebase Analytics. See [App Instance ID Requirement](#app-instance-id-requirement) for setup instructions.
</Warning>

## Features

- **Standard Ecommerce Events**: Uses Firebase's standard `purchase` and `refund` events for revenue tracking
Expand Down Expand Up @@ -76,20 +80,20 @@ Firebase requires separate credentials for iOS and Android apps since each platf

## App Instance ID Requirement

**Critical**: The Firebase integration requires `firebaseAppInstanceId` to be set in the user's `userAttributes` from your client app. This is the unique installation identifier from the Firebase Analytics SDK.
**Critical**: The Firebase integration requires the Firebase App Instance ID to be set from your client app using `setIntegrationAttributes`. This is the unique installation identifier from the Firebase Analytics SDK, and without it events will be silently skipped.

### How to Set It Up

1. In your app, retrieve the Firebase App Instance ID:
Retrieve the Firebase App Instance ID and pass it to Superwall using `setIntegrationAttributes`:

**iOS (Swift):**
```swift
import FirebaseAnalytics

Analytics.appInstanceID { appInstanceId, error in
if let appInstanceId = appInstanceId {
Superwall.shared.setUserAttributes([
"firebaseAppInstanceId": appInstanceId
Superwall.shared.setIntegrationAttributes([
.firebaseInstallationId: appInstanceId
])
}
}
Expand All @@ -100,15 +104,27 @@ Analytics.appInstanceID { appInstanceId, error in
import com.google.firebase.analytics.FirebaseAnalytics

FirebaseAnalytics.getInstance(context).appInstanceId.addOnSuccessListener { appInstanceId ->
Superwall.instance.setUserAttributes(mapOf(
"firebaseAppInstanceId" to appInstanceId
Superwall.instance.setIntegrationAttributes(mapOf(
IntegrationAttribute.FIREBASE_APP_INSTANCE_ID to appInstanceId
))
}
```

**Flutter (Dart):**
```dart
import 'package:firebase_analytics/firebase_analytics.dart';

final appInstanceId = await FirebaseAnalytics.instance.appInstanceId;
if (appInstanceId != null) {
await Superwall.shared.setIntegrationAttributes({
IntegrationAttribute.firebaseAppInstanceId: appInstanceId,
});
}
```

### What Happens Without It

If `firebaseAppInstanceId` is not found in `userAttributes`:
If the Firebase App Instance ID is not set via `setIntegrationAttributes`:
- The event is **skipped** (not sent to Firebase)

### Revenue Events (Standard Ecommerce)
Expand Down Expand Up @@ -320,7 +336,7 @@ After sending events, verify in Firebase:

## Best Practices

1. **Set App Instance ID Early**: Call `setUserAttributes` with `firebaseAppInstanceId` as soon as the app launches to ensure all subscription events are tracked.
1. **Set App Instance ID Early**: Call `setIntegrationAttributes` with the Firebase App Instance ID as soon as the app launches to ensure all subscription events are tracked.

2. **Separate Environments**: Use separate Firebase projects (or at minimum, separate measurement streams) for sandbox and production to keep analytics clean.

Expand Down Expand Up @@ -363,7 +379,7 @@ Calculate: Sum of value per user

### Events Not Appearing in Firebase

1. **Check App Instance ID**: Ensure `firebaseAppInstanceId` is set in `userAttributes`
1. **Check App Instance ID**: Ensure the Firebase App Instance ID is set via `setIntegrationAttributes`
2. **Verify Platform Credentials**: Confirm the correct platform credentials are configured:
- iOS events (App Store) require `ios_firebase_app_id` + `ios_api_secret`
- Android events (Play Store) require `android_firebase_app_id` + `android_api_secret`
Expand All @@ -381,14 +397,14 @@ Calculate: Sum of value per user

**Single-Platform Apps**: If your app is iOS-only or Android-only, you only need to configure credentials for that platform. Events from unconfigured platforms will be skipped (this is expected behavior).

### Missing firebaseAppInstanceId
### Missing Firebase App Instance ID

**Problem**: Events are being skipped with warning about missing `firebaseAppInstanceId`
**Problem**: Events are being skipped with warning about missing Firebase App Instance ID

**Solutions**:
1. Ensure your app calls `FirebaseAnalytics.getAppInstanceId()` and passes it to Superwall
2. Verify `setUserAttributes` is called before any purchases occur
3. Check that the attribute key is exactly `firebaseAppInstanceId` (case-sensitive)
1. Ensure your app calls `FirebaseAnalytics.getAppInstanceId()` (or platform equivalent) and passes it to Superwall via `setIntegrationAttributes`
2. Verify `setIntegrationAttributes` is called before any purchases occur
3. Use the correct integration attribute key for your platform (e.g., `.firebaseInstallationId` on iOS, `IntegrationAttribute.firebaseAppInstanceId` on Flutter)

### Revenue Not Tracking

Expand Down
59 changes: 57 additions & 2 deletions src/components/ChangelogTimeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,59 @@ function filterToRecentMonths(
});
}

/**
* Deduplicate entries by path when they fall within a short time window of each
* other (e.g. same deploy / commit batch). Genuine updates days or weeks apart
* are preserved as separate entries.
*/
const DEDUP_WINDOW_MS = 24 * 60 * 60 * 1000; // 24 hours

function deduplicateByPath(
entries: ChangelogDataType['entries']
): ChangelogDataType['entries'] {
const grouped = new Map<string, (typeof entries)[number][]>();

for (const entry of entries) {
if (!grouped.has(entry.path)) {
grouped.set(entry.path, []);
}
grouped.get(entry.path)!.push(entry);
}

const result: (typeof entries)[number][] = [];

for (const group of grouped.values()) {
group.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());

let kept = group[0];
result.push(kept);

for (let i = 1; i < group.length; i++) {
const gap = new Date(kept.date).getTime() - new Date(group[i].date).getTime();
if (gap > DEDUP_WINDOW_MS) {
kept = group[i];
result.push(kept);
}
}
}

result.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime());

// Only the oldest entry per path can be "added" (New); later ones become "modified" (Update)
const firstSeen = new Map<string, number>();
for (let i = result.length - 1; i >= 0; i--) {
if (!firstSeen.has(result[i].path)) {
firstSeen.set(result[i].path, i);
}
}
return result.map((entry, i) => {
if (entry.changeType === 'added' && firstSeen.get(entry.path) !== i) {
return { ...entry, changeType: 'modified' as const };
}
return entry;
});
}

/**
* Get the month key for a date (e.g., "2026-01").
*/
Expand Down Expand Up @@ -153,8 +206,10 @@ export function ChangelogTimeline() {
return <EmptyState />;
}

// Only show entries from the last 3 months
const recentEntries = filterToRecentMonths(data.entries, MONTHS_TO_SHOW);
// Only show entries from the last 3 months, deduplicated per page
const recentEntries = deduplicateByPath(
filterToRecentMonths(data.entries, MONTHS_TO_SHOW)
);

if (recentEntries.length === 0) {
return <EmptyState />;
Expand Down
Loading