Skip to content
Tolinku
Tolinku
Sign In Start Free
Use Cases · · 6 min read

Re-Onboarding Returning Users with Deep Links

By Tolinku Staff
|
Tolinku e commerce deep linking dashboard screenshot for use cases blog posts

Users leave and come back. Some return after a week, some after months. When they return, they face a problem: the app may have changed, their context is gone, and they've forgotten how things work. Re-onboarding is the process of getting returning users back up to speed without treating them like new users. Deep links are the mechanism that brings them back, and the context in those links determines what they see.

For building growth loops, see App Growth Loops: Building Self-Sustaining Growth. For budget-friendly strategies, see App Growth for Startups: Budget-Friendly Strategies. For win-back campaign design, see Re-Engagement Campaigns: Winning Back Lapsed Users.

Identifying Returning Users

User Segments

Segment Absence Duration Likely State Re-Onboarding Need
Brief lapse 7-14 days Remember the app Minimal (what's new)
Moderate lapse 15-30 days Partially remember Light re-onboarding
Long lapse 31-90 days Forgot most features Moderate re-onboarding
Churned 90+ days Don't remember Near-full re-onboarding

Detection

async function detectReturningUser(userId) {
  const user = await getUser(userId);
  const daysSinceLastActive = daysBetween(user.lastActiveAt, new Date());

  if (daysSinceLastActive < 7) {
    return { type: 'active', needsReOnboarding: false };
  }

  if (daysSinceLastActive < 15) {
    return { type: 'brief_lapse', needsReOnboarding: false, showWhatsNew: true };
  }

  if (daysSinceLastActive < 31) {
    return { type: 'moderate_lapse', needsReOnboarding: true, level: 'light' };
  }

  if (daysSinceLastActive < 91) {
    return { type: 'long_lapse', needsReOnboarding: true, level: 'moderate' };
  }

  return { type: 'churned', needsReOnboarding: true, level: 'full' };
}

Re-Onboarding Flows

Brief Lapse (7-14 Days): What's New

For users who were recently active, just highlight changes:

function WhatsNewScreen({ changes }) {
  return (
    <BottomSheet>
      <Heading>What's new since you were last here</Heading>

      {changes.map(change => (
        <ChangeItem key={change.id}>
          <Icon name={change.icon} />
          <View>
            <Text style={styles.changeTitle}>{change.title}</Text>
            <Text style={styles.changeDescription}>{change.description}</Text>
          </View>
        </ChangeItem>
      ))}

      <Button onPress={dismiss}>Got It</Button>
    </BottomSheet>
  );
}

async function getChanges(lastActiveDate) {
  const allChanges = await getAppChangelog();
  return allChanges.filter(c => new Date(c.date) > lastActiveDate);
}

Moderate Lapse (15-30 Days): Guided Return

Show the user their existing data and remind them of key features:

function ModerateReOnboarding({ user }) {
  return (
    <View>
      <WelcomeBackScreen
        userName={user.firstName}
        lastSeen={formatRelative(user.lastActiveAt)}
      />

      <DataSummary
        items={user.itemCount}
        projects={user.projectCount}
        lastActivity={user.lastActivity}
      />

      <QuickActions
        actions={[
          { label: 'Continue where you left off', deepLink: user.lastScreen },
          { label: 'See what is new', deepLink: '/changelog' },
          { label: 'Start fresh', deepLink: '/home' },
        ]}
      />
    </View>
  );
}

Long Lapse (31-90 Days): Feature Reintroduction

The user has likely forgotten features. Show a condensed version of the onboarding:

function LongLapseReOnboarding({ user }) {
  const newFeatures = getFeaturesSince(user.lastActiveAt);
  const coreFeatures = getCoreFeatures();

  return (
    <View>
      <WelcomeBackScreen
        userName={user.firstName}
        message="A lot has happened since you were last here."
      />

      {newFeatures.length > 0 && (
        <NewFeaturesCarousel features={newFeatures} />
      )}

      <FeatureRefresher
        features={coreFeatures}
        format="quick_tips" // Short tooltips, not full tour
      />

      <DataPreservation
        message="All your data is still here."
        stats={{
          projects: user.projectCount,
          items: user.itemCount,
        }}
      />

      <Button onPress={() => navigation.navigate('Home')}>
        Let's Go
      </Button>
    </View>
  );
}

Churned (90+ Days): Near-Full Re-Onboarding

Users absent for 3+ months need substantial re-onboarding, but not from scratch:

function ChurnedReOnboarding({ user }) {
  return (
    <View>
      <WelcomeBackScreen
        userName={user.firstName}
        message="Welcome back! Here is a quick refresher."
      />

      {/* Condensed feature tour (3 screens, not 7) */}
      <CondensedTour
        features={getTopFeatures(3)}
        format="highlight"
      />

      {/* Check if their data is still relevant */}
      <DataCheck
        onKeepData={() => navigation.navigate('Home')}
        onStartFresh={() => resetUserData(user.id)}
      />

      {/* Offer to update preferences */}
      <PreferenceRefresh user={user} />
    </View>
  );
}

The channels you use to bring users back (push, email, in-app) each require different strategies. For push-specific guidance, see Push Notification Strategy for Mobile Apps.

async function sendWinBackCampaign(userId, segment) {
  const deepLink = await Tolinku.createLink({
    path: '/welcome-back',
    params: {
      source: 'win_back',
      segment: segment.type,
      offer: segment.offer,
    },
  });

  const messages = {
    moderate_lapse: {
      push: { title: 'We have been busy!', body: '3 new features since you last visited.' },
      email: { subject: 'See what is new in [App]' },
    },
    long_lapse: {
      push: { title: 'We missed you!', body: 'Come back and see what has changed.' },
      email: { subject: 'A lot has changed. Come take a look.' },
    },
    churned: {
      push: { title: 'Your data is waiting', body: 'Your projects are still here. Come back anytime.' },
      email: { subject: 'Your [App] account is still active' },
    },
  };

  const msg = messages[segment.type];

  // Send push first, email as follow-up
  await sendPush(userId, {
    ...msg.push,
    data: { deepLink: deepLink.url },
  });

  // Email 24h later if they don't return
  scheduleEmail(userId, {
    ...msg.email,
    deepLink: deepLink.url,
    sendAt: addHours(24),
    condition: () => userNotActive(userId, '24h'),
  });
}
async function handleWelcomeBackDeepLink(params) {
  const user = await getCurrentUser();

  // Track the re-engagement
  analytics.track('re_engagement_deep_link_opened', {
    segment: params.segment,
    source: params.source,
    daysSinceLastActive: daysSince(user.lastActiveAt),
  });

  // Apply any offer
  if (params.offer) {
    await applyReEngagementOffer(user.id, params.offer);
  }

  // Route to appropriate re-onboarding
  const segment = detectReturningUser(user.id);
  if (segment.needsReOnboarding) {
    navigation.navigate('ReOnboarding', { level: segment.level });
  } else {
    navigation.navigate('Home');
  }
}

Bring users back with specific content relevant to their interests:

async function generatePersonalizedWinBackLink(userId) {
  const user = await getUser(userId);
  const interests = user.interests || [];
  const newContent = await getNewContentForInterests(interests, user.lastActiveAt);

  if (newContent.length > 0) {
    const topContent = newContent[0];

    return await Tolinku.createLink({
      path: `/content/${topContent.id}`,
      params: {
        source: 'win_back',
        content_type: topContent.type,
      },
      ogTitle: `New in [App]: ${topContent.title}`,
      ogDescription: topContent.preview,
    });
  }

  // Fallback to generic win-back
  return await Tolinku.createLink({
    path: '/welcome-back',
    params: { source: 'win_back' },
  });
}

Re-Onboarding Offers

Incentives by Segment

Segment Offer Type Example
Moderate lapse Feature unlock "Try our new Premium feature free for 7 days"
Long lapse Discount "Welcome back! 30% off Premium this month"
Churned Extended trial "Start a new 14-day free trial"
Churned (former paying) Win-back discount "Get 50% off for 3 months"

Offer Implementation

async function applyReEngagementOffer(userId, offerCode) {
  const offers = {
    COMEBACK_TRIAL: {
      type: 'trial_extension',
      days: 7,
      features: ['premium'],
    },
    COMEBACK_30: {
      type: 'discount',
      percent: 30,
      duration: '1_month',
    },
    COMEBACK_50: {
      type: 'discount',
      percent: 50,
      duration: '3_months',
      minAbsenceDays: 90,
    },
  };

  const offer = offers[offerCode];
  if (offer === undefined) return;

  // Validate eligibility
  const user = await getUser(userId);
  if (offer.minAbsenceDays) {
    const absence = daysSince(user.lastActiveAt);
    if (absence < offer.minAbsenceDays) return;
  }

  await applyOffer(userId, offer);
  await notifyUser(userId, `Welcome back! Your ${offer.type} has been applied.`);
}

Account State Management

Handling Changed Data

Things that may have changed while the user was away:

async function reconcileReturningUser(user) {
  const changes = [];

  // Check if any connected accounts expired
  const connectedAccounts = await getConnectedAccounts(user.id);
  for (const account of connectedAccounts) {
    if (account.tokenExpired) {
      changes.push({
        type: 'reconnect_required',
        account: account.name,
        action: () => navigation.navigate('ReconnectAccount', { accountId: account.id }),
      });
    }
  }

  // Check if subscription lapsed
  if (user.subscriptionStatus === 'expired') {
    changes.push({
      type: 'subscription_expired',
      action: () => navigation.navigate('Resubscribe'),
    });
  }

  // Check for pending notifications
  const unread = await getUnreadNotifications(user.id);
  if (unread.length > 0) {
    changes.push({
      type: 'pending_notifications',
      count: unread.length,
      action: () => navigation.navigate('Notifications'),
    });
  }

  return changes;
}

Measuring Re-Onboarding

Key Metrics

Metric Definition Target
Win-back rate Returned users / Win-back messages sent 5-15%
Re-activation rate Active D7 after return / Returned users 30-50%
Re-onboarding completion Completed / Shown re-onboarding 70-90%
Re-churn rate Churned again within 30 days / Returned < 50%
Revenue recovery Revenue from returned users / Win-back cost > 3x

Cohort Tracking

async function reOnboardingCohortAnalysis() {
  const segments = ['moderate_lapse', 'long_lapse', 'churned'];

  for (const segment of segments) {
    const returned = await getReturnedUsers(segment, { last: '90d' });
    const reActivated = returned.filter(u => u.activeDay7AfterReturn);
    const reChurned = returned.filter(u => u.churnedWithin30Days);

    console.log(segment, {
      returned: returned.length,
      reActivationRate: (reActivated.length / returned.length * 100).toFixed(1) + '%',
      reChurnRate: (reChurned.length / returned.length * 100).toFixed(1) + '%',
    });
  }
}

If the re-churn rate is above 50%, the re-onboarding isn't addressing why users left in the first place. Investigate the original churn reasons before optimizing the re-onboarding flow. For proactive strategies on preventing churn before it happens, see Reducing App Churn: Strategies That Actually Work.

For deep linking features, see Tolinku deep linking. For onboarding use cases, see the onboarding documentation.

Get deep linking tips in your inbox

One email per week. No spam.

Ready to add deep linking to your app?

Set up Universal Links, App Links, deferred deep linking, and analytics in minutes. Free to start.