App lifecycle
Lifecycle hooks report when the app document is hidden or shown and when the user asks to leave through Jest's platform controls.
Register listeners
- HTML5
- Unity
Register hooks after SDK initialization. Every registration returns an unsubscribe function:
await JestSDK.init();
const unsubscribeHide = JestSDK.lifecycle.onHide(() => {
game.pause();
audio.pause();
});
const unsubscribeShow = JestSDK.lifecycle.onShow(() => {
game.resume();
audio.resume();
});
const unsubscribeExit = JestSDK.lifecycle.onExitRequested(async () => {
await saveProgress();
});
function destroyGame() {
unsubscribeHide();
unsubscribeShow();
unsubscribeExit();
}
Each method supports multiple listeners and returns a function that removes only that registration. Keep subscriptions active for the app lifetime, then remove them during teardown. Calling the removal function more than once is safe.
JestSDK.Instance.Lifecycle exposes the hooks as System.Action events, so subscribe with += and
unsubscribe with -=. Each event supports multiple handlers.
JestSDK.Instance.Lifecycle exists before Init() is called, so subscribe from OnEnable and
unsubscribe from OnDisable. Handlers only start firing once Init() completes, and pairing the
Unity callbacks this way keeps the subscription tied to the component's own lifetime:
using com.jest.sdk;
using UnityEngine;
public class LifecycleListener : MonoBehaviour
{
// Your game's own pause system, not an SDK type.
[SerializeField] private GamePause m_pause;
[SerializeField] private int m_currentCheckpoint;
private void OnEnable()
{
JestSDK.Instance.Lifecycle.OnHide += HandleHide;
JestSDK.Instance.Lifecycle.OnShow += HandleShow;
JestSDK.Instance.Lifecycle.OnExitRequested += HandleExitRequested;
}
private void OnDisable()
{
JestSDK.Instance.Lifecycle.OnHide -= HandleHide;
JestSDK.Instance.Lifecycle.OnShow -= HandleShow;
JestSDK.Instance.Lifecycle.OnExitRequested -= HandleExitRequested;
}
private void HandleHide()
{
m_pause.PauseForVisibility();
}
private void HandleShow()
{
m_pause.ResumeForVisibility();
}
private void HandleExitRequested()
{
JestSDK.Instance.Player.Set("checkpoint", m_currentCheckpoint);
}
}
GamePause here is your game's own pause system, and that separation is the point: route hide and show
through whatever already owns pausing instead of writing Time.timeScale or AudioListener.pause from
the handler. Those are global, and a handler that overwrites them fights every other system that pauses
— slow motion, a settings menu, another lifecycle handler — with the outcome decided by subscription
order. A pause system that counts its reasons for pausing can absorb a visibility pause without
discarding the rest.
Put the component on an object that lives as long as the session, such as a DontDestroyOnLoad object,
so the subscription spans the whole game. A hide event fires on the visibility transition only, so a
component that is disabled and re-enabled while the document is still hidden receives no replacement
event — leave it enabled rather than trying to reconstruct the hidden state from the Unity callbacks.
If you prefer to subscribe once initialization has finished, keep the handlers and the -= calls as
members of the component rather than locals of the initializing method — Unity only calls OnDestroy
on the component itself, and a local OnDestroy never runs. The component can also be destroyed
while Init() is still awaited, so check that it is still alive before subscribing:
private async void Start()
{
await JestSDK.Instance.Init();
if (this == null)
{
return;
}
JestSDK.Instance.Lifecycle.OnHide += HandleHide;
}
private void OnDestroy()
{
JestSDK.Instance.Lifecycle.OnHide -= HandleHide;
}
Removing a handler that was never added is a no-op, so teardown is safe even when initialization never completed. Handlers stay attached to the SDK singleton until they are removed, so a destroyed component that never unsubscribes keeps receiving events.
Hide
Runs when the browser changes the app document from visible to hidden, such as when the user switches tabs, backgrounds the browser, or locks the device. It does not run for the document's initial visibility state.
Use it to pause the game loop, physics, animation, and audio.
The hide and show hooks are the SDK's surface for the document's visibilitychange
event, and Jest adds nothing to when they fire. An app that also observes browser
visibility another way sees every transition twice, so subscribe one way or
the other, not both.
- HTML5
- Unity
JestSDK.lifecycle.onHide(listener)
JestSDK.Instance.Lifecycle.OnHide
Show
Runs when the app document changes from hidden back to visible. It does not run on initial startup; SDK initialization is the startup boundary.
Use it to resume work stopped by the hide hook, refresh time-sensitive state, and reconcile elapsed time.
- HTML5
- Unity
JestSDK.lifecycle.onShow(listener)
JestSDK.Instance.Lifecycle.OnShow
Exit requested
Runs when the platform begins an exit flow, including its exit control and browser Back or mobile swipe-back navigation that Jest can intercept. It runs at the start of that flow — usually as the exit confirmation appears — and always before the user has answered it.
The user can still choose to stay, so this is an opportunity to save and not a shutdown notice. Do not tear down state, release resources, or stop the game loop from the listener; the session frequently continues. It also runs more than once in a session whenever a user asks to leave, backs out, and asks again, so keep the listener idempotent and expect to save again later.
The listener cannot cancel the exit or hold the app open past the user's confirmation. What it does get is the interval between the request and that confirmation, which is ordinary runtime rather than a teardown window. Start the save when the event runs.
This event only represents an exit flow the platform can intercept. Closing the tab, terminating the browser, or an operating-system shutdown may not produce a final event. Back navigation that leaves the Jest document directly, such as returning to an external referral page, may not produce one either. A few in-platform flows also navigate without a confirmation, in which case the event runs with no interval behind it.
- HTML5
- Unity
JestSDK.lifecycle.onExitRequested(listener)
Do not defer the save to pagehide or beforeunload, where the document is
already going away.
JestSDK.Instance.Lifecycle.OnExitRequested
Ordering
The hooks are independent signals, not steps in a teardown sequence. Visibility comes from the browser and the exit request comes from the platform, so there is no guaranteed ordering between them and no pair is mutually exclusive. A platform exit runs the exit request with no visibility change; backgrounding the browser runs the hide hook with no exit request; locking the device while the exit confirmation is open runs both, in whichever order the browser reports. A hook that does not run is not evidence that the other one failed.
Saving on exit
Treat the exit event as the last of several saves rather than the only one. Data written through the player data API is sent to the Jest page, which performs the storage write. That page outlives the app document, so an update already on its way is not lost when the app is torn down. An update is not sent immediately while an earlier one is still unacknowledged — the SDK coalesces it into the next message — so a write issued at the very end of the exit interval can still go down with the document. Persist progress as the player earns it, and use this event to capture whatever has changed since the last save.
- HTML5
- Unity
JestSDK.data.set(key, value)
JestSDK.lifecycle.onExitRequested(() => {
JestSDK.data.set("checkpoint", currentCheckpoint);
});
JestSDK.Instance.Player.Set<T>(key, value)
private void HandleExitRequested()
{
JestSDK.Instance.Player.Set("checkpoint", m_currentCheckpoint);
}
Asynchronous listeners
- HTML5
- Unity
Listeners may return a promise. The SDK starts every listener immediately and
reports synchronous errors and promise rejections without interrupting the
other listeners. Awaiting inside a listener still sequences that listener's
work, but it does not delay the exit confirmation or navigation. If navigation
unloads the document first, the promise may not settle and code after an
await may not run.
Start essential work before the first await.
JestSDK.lifecycle.onExitRequested(async () => {
JestSDK.data.set("checkpoint", currentCheckpoint);
await analytics.flush();
});
Handlers are System.Action and run synchronously; start the save inside the handler and let
Player.Set carry it to the Jest page.
Testing locally
- HTML5
- Unity
Running outside Jest.com puts the SDK in mock mode. Open the
JestSDK debug menu and use Request Exit under App Lifecycle to send the
platform exit request to your listeners. onHide and onShow come from real
browser visibility, so switch tabs or lock the device to trigger them.
Running a WebGL build outside Jest.com puts the SDK in mock mode. Open the
JestSDK debug menu and use Request Exit under App Lifecycle to send the platform exit
request to your handlers. The Unity Editor mock has no exit-request trigger. OnHide and OnShow
come from real browser visibility, so in a WebGL build switch tabs or lock the device to trigger them.