Skip to content
Sendora Cloud
Create account
Identity

Adopting held guests

From React Native 1.41, web 3.24, iOS 5.4 and Android 4.26, signing in from a guest account no longer retires the guest. The SDK holds it on the device, and your app decides what happens to it. An app keeps working on the new SDK with no code change, but the six changes below decide whether a player keeps their progress.

The examples are React Native. Web, iOS and Android ship the same verbs. Current releases: React Native 1.42.0, web 3.25.0, iOS 5.5.0, Android 4.27.0.

What changed

  • A sign-in holds the guest. The device keeps the guest's session in a held slot (up to five), the SDK emits guest_held, and the server sends auth.guest_held. Nothing is deleted. Sendora runs no timer on held guests: a held session lasts as long as your project's refresh lifetime, which is "Never" unless you changed it.
  • Linking is the only upgrade. { link: true }, link() and signUp() promote the guest in place, so the user id stays the same.
  • Three verbs end a held guest: restoreHeldGuest(id) goes back to it, discardHeldGuest(id) deletes it, and claimHeldGuest(id) merges its data into the signed-in account. Each has a webhook; see Identity in your backend.

1. Link at launch only from a guest

A launch-time Game Center or Play Games call with { link: true } promotes a guest in place. Gate it on status() === "anonymous": on an identified session the same call attaches the identity to the current account, which belongs behind an explicit "Link Game Center" button, not a launch hook.

import SendoraCloud, { CredentialInUseError } from "@sendoracloud/sdk-react-native";

if (SendoraCloud.auth.status() === "anonymous") {
  try {
    await SendoraCloud.auth.signInWithGameCenter({ link: true });
  } catch (err) {
    if (err instanceof CredentialInUseError) {
      // This player already has an account. Ask before switching to it.
      showSwitchAccountPrompt();
    } else {
      throw err;
    }
  }
}

A guest is never asked to re-authenticate. RECENT_AUTH_REQUIRED can only come from the explicit button on an identified session. If a sign-in does switch accounts, reload your local save for the new user id.

2. Decide what happens to a held guest

Listen for guest_held. A guest with no progress can be discarded by the app. A guest with progress belongs to the player, so ask them.

import SendoraCloud from "@sendoracloud/sdk-react-native";

SendoraCloud.auth.onAuthStateChanged(async (change) => {
  if (change.event !== "guest_held") return;
  const guestId = change.userId;
  if (!(await hasLocalProgress(guestId))) {
    await SendoraCloud.auth.discardHeldGuest(guestId);
    return;
  }
  const keep = await askPlayer("We found a guest account on this device. Keep its progress?");
  if (keep) await SendoraCloud.auth.restoreHeldGuest(guestId);
  else await SendoraCloud.auth.discardHeldGuest(guestId);
});

If the player keeps a guest whose Game Center identity belongs to another account, remember that choice and skip the launch link for that guest, or step 1 will ask them again on every launch.

3. Merge only if your app merges data

claimHeldGuest(id) moves the guest's events, profile, entities and push tokens into the signed-in account and deletes the guest (auth.user_merged). Call it only if your backend merges progress too.

import SendoraCloud from "@sendoracloud/sdk-react-native";

const held = SendoraCloud.auth.getHeldGuests();
if (held.length === 1) {
  await SendoraCloud.auth.claimHeldGuest(held[0].userId);
}

⚠ Apps that never merge must remove any merge() call. On the new SDK, the deprecated merge() with a held guest's token becomes claimHeldGuest(), which deletes that guest. A template that ran merge() after every sign-in would now merge and delete the held guest every time, and a backend that keys progress on the guest's user id without handling auth.user_merged loses that progress.

4. Retire the takeover handler

Delete any onDeviceTakeover listener, and read getHeldGuests() where you read getLastAnonRetirement(). Your backend keeps handling auth.device_takeover until every installed copy runs the new SDK. Add handlers for auth.guest_held, auth.user_merged and auth.user_deleted with reason: "guest_discarded". Webhook delivery is at least once with no ordering, so dedupe on X-Sendora-Delivery (Webhooks).

5. Set the project guard

Pass your project id as expectedProjectId. A session that belongs to a different project is then withheld instead of used. Record the old user id in your backend before discarding it.

import SendoraCloud from "@sendoracloud/sdk-react-native";

await SendoraCloud.init({
  apiKey: "pk_prod_…",
  expectedProjectId: "<your project id>",
});

SendoraCloud.onIdentityWithheld(async (info) => {
  await recordWithheldUser(info.userId);
  await SendoraCloud.auth.discardWithheld();
});

6. Resolve held guests when an account is deleted

Deleting an account never touches a held guest. If the player asks to erase everything, say so in the call:

await SendoraCloud.auth.deleteAccount({ alsoDiscardHeldGuests: true });

Your privacy page should say that deleting an account does not delete a guest saved on the device unless the player chooses to.

Check your progress

The dashboard's Authentication → Users page shows a held-guests panel for the selected project: the SDK versions your users actually run (flagged when one is behind the current release), the guests held, merged and discarded over 7 and 30 days, and legacy takeovers from installs still on an older SDK. An app is done when its users run the current SDK and legacy takeovers stay at zero. Older SDKs keep working, so nothing breaks while installs catch up. See also Device-takeover on signIn for the legacy path.