The Web2App Funnel Where Users Pay on the Web and Open the iOS App Already Logged In, Even Before It's Installed

The Web2App Funnel Where Users Pay on the Web and Open the iOS App Already Logged In, Even Before It's Installed

# web2app# mobile# tutorial# marketing
The Web2App Funnel Where Users Pay on the Web and Open the iOS App Already Logged In, Even Before It's InstalledUtkarsh Shrivastava

I built this crazy seamless system (Not sure, how its done usually in the industry), Where we are...

I built this crazy seamless system (Not sure, how its done usually in the industry), Where we are acquiring users on the web funnel, making them pay on the web, and after purchase they land on the mobile app which is already authenticated even when it is not installed (thats the main sauce btw), hence reducing the friction of logging them again, getting rid of the apple tax, getting the retention of a native mobile app, bypassing the bitchy apple again with its ATT permission.

Apple takes up to 30% of every subscription you sell inside the app.

Read that again. Apple takes Up to a third of your revenue, for a payment sheet.
So we just moved the money to the web. Apple only gets its cut when the purchase happens inside the app.

There Is Less Friction When Nobody Has to Download Anything

Imagine someone is scrolling and they see your ad. And you want them to leave, open the App Store, download 80MB, open the app, get through onboarding, and then decide if they want to pay.

A web funnel asks for only one thing: a tap.

From Ad -> quiz flow -> paywall -> paid. They never leave the browser, and the money is in before they've installed anything.

You control your paywall, no matter how shady it is (lol)

Since the purchase is happening outside of the mobile app, apple cant do anything, you can apply whatever black marketing tips and tricks you have learnt.

So, How to actually get your users to the app with logged in state even when they haven't installed the app?

First, you need to understand one thing that the App Store is a wall.

Nothing can cross it, whether its cookies, session, or local storage. Your user was logged in on the web two minutes ago, and your app opens with zero idea who they are.

But there's one thing, one thing that really does cross the wall, and you're probably already paying for it: your attribution tool, i.e. Appsflyer.

They have this thing called deferred deep linking.

It works like this:

  1. The user taps your link.
  2. The provider records the tap and sends them to the App Store.
  3. They install and open the app.
  4. The SDK asks its server: "did anyone tap a link for this phone?"
  5. If yes, you get whatever was on the link.

So we put a one time login token on the link, and thats the whole trick.

So the flow is : pay and sign up on the web, tap Get the app, install, and the token rides the link to the app's first launch

Step 1: Mint a Token on the Web

When the logged in user taps Get the app, your backend creates a one time token for them:

const token = randomBytes(32).toString("base64url");
await db.query(
  `insert into handoff_tokens (token_hash, user_id, expires_at)
   values ($1, $2, now() + interval '1 hour')`,
  [sha256(token), session.userId],
);
Enter fullscreen mode Exit fullscreen mode

Few things to take care of during this process :

  • Get the user from their logged-in session, not from anything the browser sends. Otherwise anyone could log in as anyone.
  • Store only the hash.
  • It should have a short lifetime.
  • Keep it out of your logs.

Step 2: Put It on the Link (This Is Where I Wasted a Lot of Time)

My first version put the token on the link like a normal person

https://yourapp.onelink.me/AbCd/handoff?deep_link_value=app_handoff&token=<token>
Enter fullscreen mode Exit fullscreen mode

On Android it worked like magic. Tap, install, open, logged in.

But, On iOS it opened to the normal splash screen, and nothing happened.

I logged every SDK callback, and every fresh iOS install said the same thing:

status=FOUND  is_deferred=true  deep_link_value=app_handoff  has_token=false
Enter fullscreen mode Exit fullscreen mode

What I found was that the app knew the install came from our link. It even gave us deep_link_value, But the main token was gone.

Then I found this one line in the AppsFlyer docs:

▎ For new users, the UDL method only returns parameters relevant to deferred deep linking: deep_link_value and deep_link_sub1-10.

Since iOS 14.5, a fresh install only gets a handful of "safe" parameters. Your custom token= gets wiped out silently.

Apple again being a bitch obviously.

The fix is simple and stupid, We have to Put the token in one of the allowed slots:

https://yourapp.onelink.me/AbCd/handoff?deep_link_value=app_handoff&deep_link_sub1=<token>
Enter fullscreen mode Exit fullscreen mode

Step 3: Read It in the App

sdk.onDeepLinking((res) async {
  if (res.status != Status.FOUND) return;
  final ev = res.deepLink?.clickEvent ?? {};
  if (ev['deep_link_value'] != 'app_handoff') return;

  final token = ev['deep_link_sub1']?.toString();
  if (token == null || token.isEmpty) return;

  final session = await api.exchangeHandoff(token);
  await auth.save(session);
  navigator.pushAndRemoveUntil(homeRoute(), (_) => false);
});
Enter fullscreen mode Exit fullscreen mode

Step 4: Swap the Token for a Real Session

The app sends the token to your backend, and the backend burns it and logs the user in:

const { rows } = await db.query(
  `update handoff_tokens set used_at = now()
    where token_hash = $1 and used_at is null and expires_at > now()
    returning user_id`,
  [sha256(token)],
);
if (!rows.length) throw new Unauthorized();
return issueSession(rows[0].user_id);

Enter fullscreen mode Exit fullscreen mode

We have to check the token and burns it in a single shot, so it can never be used twice.

issueSession is whatever auth you already have. Your own JWTs, a Firebase, Supabase custom token, anything. This whole thing doesn't care.

On Supabase you can skip the table entirely. generateLink({ type: "magiclink" }) gives you a one-time token and the app calls verifyOTP. That's what we run.

You Can Test It Without Shipping to the Store (YESSS)

AppsFlyer matches the tap to the first launch. It doesn't care where the install came from.

  1. Delete the app from your iPhone.
  2. Log in on the web and tap Get the app.
  3. When the App Store opens, don't install. Open TestFlight and install your build from there.
  4. Open it.
  5. And it works.

Through this whole flow we are reducing friction at two point:

  1. From the ad to funnel.
  2. From your web app to mobile app.

Which guarantees you the best of both worlds, Acquisition of web and Retention of mobile app.

But everything has a tradeoff.

If your user stays on the web forever, chances are they’ll forget about you and just keep paying.

But once they’re on the mobile app, and it’s a low-intent user, the constant app icon and notifications can also piss them off and increase the cancellation rate.