Free trials are one of the best ways to convert users into paying subscribers — but they can backfire. When a user forgets they started a trial, the unexpected charge feels like a betrayal. They refund, leave a bad review, and never come back. Even worse, that one bad experience can make them reluctant to try any subscription app again.

A simple reminder notification a day or two before the trial converts changes the dynamic entirely. The user feels respected, not tricked. They either cancel (which they would have anyway) or they convert knowing exactly what they’re paying for. Either way, you build trust — and trust is what drives long-term retention.

I’ve written about building trial reminders before using RevenueCat and Zapier to power email-based notifications. In this tutorial, we’ll build a similar system using local and remote push notifications in React Native — though the same concepts apply to Kotlin and Swift.

How to plan trial reminders

Trial reminders don’t have to be complicated, but sending out more than one reminder builds trust, and removes uncertainty. I would recommend a lightweight three-notification pattern:

1. Activation nudge (same day)

Send a notification on the first day, highlighting a feature they have not yet tried, in order to signal that the notifications work, and to get them to experience their first “aha moment” during the trial.

2. Mid-trial reminder (two days before end)

Two days before the trial is about to expire, remind them that the trial is ongoing and that it will expire in the next few days. You will most likely see users cancelling their trial at this point — that is to be expected. That gives you a chance to capture them with a win-back offer, for example.

3. Trial-ending alert (morning of last day)

On the morning of the last day, send a transparent, helpful reminder:

“Your trial ends today. Keep access by staying on your plan — or cancel anytime.”

How to implement trial notifications in React Native

There are two approaches: local notifications (no server needed) and remote notifications (requires a small backend). We’ll walk through both, starting with local.

Local notifications approach

With local notifications, all the reminder logic lives on the device — no server needed. When a user starts a trial, you grab the trial expiration date from RevenueCat’s customerInfo.entitlements (specifically the expirationDate on the active entitlement), then schedule notifications relative to that date.

Local notifications are the simplest approach — very little code and no backend to maintain. The trade-off is that the notification schedule can only update when the user opens your app. If someone cancels their trial through the App Store settings page, for example, the app won’t know to unschedule the reminders until it’s opened again. The same applies if they switch to a plan without a trial.

Additionally, local notifications will fail if the user’s device is powered off, and orchestrating other notification channels such as email is harder with local notifications. Some Android phones also have limitations on delivering local notifications at the exact scheduled time. 

Remote notifications

The alternative to local notifications is a server-driven approach. You run a lightweight backend that listens for RevenueCat webhook events and schedules or cancels trial reminders accordingly.

This approach takes more work to set up, but it’s significantly more reliable. You can update notification content without shipping an app update, use multiple channels (push notifications and email, for instance), and cancel reminders immediately when a user unsubscribes — no app open required. The backend itself doesn’t need to be complex: a single endpoint that digests webhooks and schedules notifications is all you need.

On the client side you only need to take care to store users’ push notification token, which is something RevenueCat can actually help you manage. You can save push tokens in Attributes, from which they are available using either REST API or Webhooks.

Choosing a notification package to use

React Native does not come with notifications included, so you need to install and configure an external dependency to run them. Here are a few options that support both remote and local notifications:

  1. Expo Notifications
    Expo notifications is excellent for notifications, especially if you have an Expo project. This tutorial will use Expo Notifications for the examples.
  2. react-native-notifications
    Wix has been maintaining their own notification package for React Native for over 10 years already. It’s stable and has all the features you need for notifications.
  3. @notifee/react-native
    Notifee is a feature rich notification library for both iOS and Android. It’s especially powerful when you want rich notifications, with images, custom sounds and interactability.

Subscription reminders with local notifications

The next parts assume that you’ve installed and configured your notification package for your project.

Step 1: create a trial schedule object

We will want to remind users three different times, so let’s build a trial object that will be used for scheduling when and containing the messages we want to send. The following trialSchedule follows the 3-part trial reminder structure we discussed earlier.

1export interface TrialReminder {
2  id: string;
3  /** Ms after trial start. Used when useEndOffset is false. */
4  delayFromStart: number;
5  /** Ms before trial end. Used when useEndOffset is true. */
6  beforeEnd: number;
7  title: string;
8  body: string;
9  /** If true, schedule relative to trial end (beforeEnd). If false, schedule relative to trial start (delayFromStart). */
10  useEndOffset: boolean;
11}
12
13export const trialSchedule: TrialReminder[] = [
14  {
15    id: "trial_activation",
16    delayFromStart: 6 * 60 * 60 * 1000, // 6 hours after start
17    beforeEnd: 0,
18    title: "Your trial is active 🎉",
19    body: "Have you tried [Feature X] yet? Tap to explore what's included.",
20    useEndOffset: false,
21  },
22  {
23    id: "trial_mid_reminder",
24    delayFromStart: 0,
25    beforeEnd: 2 * 24 * 60 * 60 * 1000, // 2 days before end
26    title: "Your trial ends in 2 days",
27    body: "Just a heads-up: your free trial expires soon. Make the most of it!",
28    useEndOffset: true,
29  },
30  {
31    id: "trial_ending",
32    delayFromStart: 0,
33    beforeEnd: 8 * 60 * 60 * 1000, // morning of last day
34    title: "Your trial ends today",
35    body: "Keep access by staying on your plan — or cancel anytime.",
36    useEndOffset: true,
37  },
38];

Step 2: schedule notifications during subscription process

Scheduling the trial reminders will happen when users subscribe. We can use getCustomerInfo to check if user is running a trial:

1import * as Notifications from "expo-notifications";
2import Purchases from "react-native-purchases";
3import { trialSchedule } from "./trialSchedule";
4
5async function scheduleTrialReminders(trialEndDate: Date): Promise<void> {
6  const endMs = trialEndDate.getTime();
7
8  for (const reminder of trialSchedule) {
9    const triggerMs = reminder.useEndOffset
10      ? endMs - reminder.beforeEnd
11      : Date.now() + reminder.delayFromStart;
12
13    // Don't schedule reminders in the past
14    if (triggerMs <= Date.now()) continue;
15
16    await Notifications.scheduleNotificationAsync({
17      identifier: reminder.id,
18      content: {
19        title: reminder.title,
20        body: reminder.body,
21      },
22      trigger: { date: new Date(triggerMs) },
23    });
24  }
25}
26
27const customerInfo = await Purchases.getCustomerInfo();
28const sub = customerInfo.subscriptionsByProductIdentifier["your_product_id"];
29
30if (sub && sub.periodType === "TRIAL") {
31  const trialEnd = sub.expiresDate;   // when the trial expires
32  const willConvert = sub.willRenew;  // will it auto-convert to paid?
33  const isSandbox = sub.isSandbox;    // skip reminders for sandbox
34
35  if (trialEnd && willConvert && !isSandbox) {
36    await scheduleTrialReminders(new Date(trialEnd));
37  }
38}

Step 3: unschedule notifications on unsubscribe

1import * as Notifications from "expo-notifications";
2import Purchases from "react-native-purchases";
3import { trialSchedule } from "./trialSchedule";
4
5async function cancelTrialReminders() {
6  for (const reminder of trialSchedule) {
7    await Notifications.cancelScheduledNotificationAsync(reminder.id);
8  }
9}
10
11async function handleSubscriptionChange() {
12  const customerInfo = await Purchases.getCustomerInfo();
13  const entitlement = customerInfo.entitlements.active["your_entitlement_id"];
14
15  // Only cancel reminders if the user previously had an active entitlement
16  // but is no longer in a trial (converted to paid, cancelled, or expired)
17  if (!entitlement) {
18    // No active entitlement at all — could be a fresh user, don't cancel
19    return;
20  }
21
22  if (entitlement.periodType !== "TRIAL") {
23    // Entitlement is active but no longer a trial — cancel pending reminders
24    await cancelTrialReminders();
25  }
26}

Subscription reminders with remote notifications

Step 1: store users’ push tokens

The first step to adding remote trial notifications is to track the users’ push tokens. This is made easy with RevenueCat Attributes, where you can store additional structured information about customers. These attributes can later be read using the REST API.

If you take a look at the documentation for Attributes, you see that both $apnsTokens and $fcmTokens are reserved attributes. This means that we can store push tokens in RevenueCat as well. Doing that through code is just a few lines:

1import * as Notifications from "expo-notifications";
2import Purchases from "react-native-purchases";
3
4async function registerPushToken() {
5  const { status } = await Notifications.requestPermissionsAsync();
6  if (status !== "granted") return;
7
8  const devicePushToken = await Notifications.getDevicePushTokenAsync();
9  await Purchases.setPushToken(devicePushToken.data);
10
11  // Optionally store Expo push token for Expo's push service.
12  // Note: getExpoPushTokenAsync() requires a projectId in bare workflow.
13  // Find yours at https://expo.dev under your project settings.
14  const expoPushToken = await Notifications.getExpoPushTokenAsync({
15    projectId: "your-expo-project-id",
16  });
17  await Purchases.setAttributes({
18    $expoPushToken: expoPushToken.data,
19  });
20}

That is all that is needed from the client side, as long as you have configured your notifications correctly. The rest of the tutorial will focus on what needs to happen on the server side.

Step 2: build a notification backend

All of the logic for trial reminders will happen on the backend side. For this we need to build an endpoint that accepts webhooks from RevenueCat and then schedules notifications based on those. To understand which types of Webhook events we need to support, we can take a look at the webhook sample events.

In this example we are going to use a Cloudflare worker, but it’s not a requirement. You could also build this using Zapier and OneSignal.

Here’s some example code:

1// Cloudflare Worker
2export default {
3  async fetch(request: Request, env: Env): Promise<Response> {
4    const authHeader = request.headers.get("Authorization");
5    if (authHeader !== `Bearer ${env.RC_WEBHOOK_SECRET}`) {
6      return new Response("Unauthorized", { status: 401 });
7    }
8
9    const payload = await request.json();
10    const {
11      type,
12      app_user_id,
13      period_type,
14      purchased_at_ms,
15      expiration_at_ms,
16      cancel_reason,
17      subscriber_attributes,
18    } = payload.event;
19
20    if (type === "INITIAL_PURCHASE" && period_type === "TRIAL") {
21      // New trial started — schedule all three reminders
22      await scheduleTrialReminders(env, {
23        userId: app_user_id,
24        purchasedAt: purchased_at_ms,
25        expiresAt: expiration_at_ms,
26        pushToken: subscriber_attributes?.$expoPushToken?.value,
27      });
28    } else if (type === "CANCELLATION" && cancel_reason === "UNSUBSCRIBE") {
29      // User explicitly cancelled during trial — cancel pending reminders
30      await cancelTrialReminders(env, app_user_id);
31    } else if (type === "EXPIRATION") {
32      // Trial or subscription expired without renewal — cancel pending reminders
33      await cancelTrialReminders(env, app_user_id);
34    } else if (type === "TRIAL_CONVERTED") {
35      // Trial successfully converted to paid — no need to send trial-ending reminders
36      await cancelTrialReminders(env, app_user_id);
37    }
38
39    return new Response("OK", { status: 200 });
40  },
41
42  // Cron: runs hourly to send due notifications
43  async scheduled(event: ScheduledEvent, env: Env) {
44    await sendDueNotifications(env);
45  },
46};
47
48// scheduleTrialReminders, cancelTrialReminders, and sendDueNotifications
49// are backend helpers you implement to store/retrieve scheduled notifications
50// and dispatch them via your push provider (e.g. Expo Push API, FCM, APNs).

Let’s go through the main functions of the code.

Unwrapping the webhook payload is the first step.

The POST request from RevenueCat webhook is sent to our worker which then unwraps the contents. In this case the interesting parts are:

  1. period_type, tells us if the subscription is TRIAL type
  2. purchased_at_ms, tells us when user made the purchase
  3. expiration_at_ms, tells us when the trial will convert
  4. subscriber_attributes, from here we can get the push notification token to use

Scheduling notifications

In our code we check first if the purchase is trial type, then we schedule a notification 6 hours from that if the purchase was made before 12pm, otherwise we schedule it for the next day. Then we schedule a mid-trial reminder 2 days before the trial ends. Last scheduled reminder is set for the morning of the expiring subscription. If the subscription expires in the morning, we schedule it for the evening of the previous day.

Unscheduling notifications

The last logic we have for the apps is that we unschedule the notification if the user cancels the trial. In this case we monitor for the cancel_reason being UNSUBSCRIBE and then remove the scheduled notifications.

Running logic to send notifications periodically

The last part of the code is that we run a different worker every hour to send notifications to users based on their scheduled times. This is done through a cron job.

Step 3: enable webhooks in the RevenueCat dashboard

Navigate to the RevenueCat dashboard, and select the Integrations tab from the sidebar. Under Core Tools, select Webhooks. Click Add Webhook and set the Delivery URL to your worker endpoint (e.g. https://your-worker.your-subdomain.workers.dev). Set the Authorization header to match the RC_WEBHOOK_SECRET you configured in your worker. Make sure to enable at minimum the INITIAL_PURCHASE, CANCELLATION, EXPIRATION, and TRIAL_CONVERTED event types.

Wrap-up

Trial notifications are one of the fastest, lowest-effort ways to improve trial quality and conversion. With clear data (purchasedDate, expiresDate) and a small amount of React Native code, you can build a trust-first notification flow that feels more like Blinkist — transparent, helpful, and user-centred.

Start with the three-message sequence, keep the messaging honest, and iterate based on user behavior. You’ll see the lift.