Appearance
Ways to start the app
Every AndroidIgpapi call is one app process, from birth to death. You pick the entry that matches what the real app was doing before your program starts: a new install, a cold start of a known user, or a return to a screen you left. Each entry takes exactly one object: the process options, the stored value (checkpoint, or snapshot for launch), the expected screen, the otherwise branches, an optional signal and main.
Choosing in one sentence. A new account or a broken session: install. Every later run: launch. Continuing a half-done flow such as a code screen in another process: resumeSilently.
main is a function from the screen to an Effect (home => Effect.gen(function* () { … })). Inside it you write Effect code; outside it you get a plain Promise. The call resolves with main's result, and a typed failure of the start or of main rejects with the very same tagged error object, so error._tag and instanceof work outside. Saving and loading state is plain JavaScript around the await: take screen.checkpoint() inside main, write it to your database outside. The handles live as long as main does and must not be used after it ends. There is no instance to keep and no close to call. Nothing is built, and nothing is sent, before the call.
The entries
AndroidIgpapi.install({ …options, main, otherwise? })creates a fresh install on a virtual device (seednames the device, a random one when omitted), launches the app, and runsmainwith the login screen. Whenmainends the process closes, on success, on failure or when interrupted.AndroidIgpapi.launch({ checkpoint | snapshot, screen?, …options, main, otherwise? })is a cold start of the app for a stored user.maingets the screen the app shows first: for a checkpoint the type still names, the screen that checkpoint leads to; for a snapshot or a checkpoint read back from a file, the unionAndroidAppLaunchFirstScreen. Name the screen you expect andmaingets exactly that handle:AndroidIgpapi.launch({ checkpoint: saved, screen: FeedTimelineScreen, main: feed => … }).AndroidIgpapi.resumeSilently({ checkpoint, screen?, …options, main, otherwise? }),resumeFromBackground(…)andresumeByReentering(…)continue the checkpointed process on the checkpoint's own screen (below). There is no plainresume: you always choose whether requests go out.
The stored value is an object, never JSON text: parse what your storage returns (JSON.parse(row.checkpoint)) and pass the object (Keep a session).
Which one to choose
checkpoint is screen.checkpoint() of an earlier program; snapshot is ExportSnapshot (or a hand-built one). A part a snapshot lacks is the Simulator's own.
| Entry | In real-app terms | Needs | What goes out at start | What main receives | Choose it when |
|---|---|---|---|---|---|
install | A new install, opened for the first time | Nothing; seed makes the device repeatable | The pre-login launch | The login screen | A new device, or logging in from nothing |
launch from a checkpoint | The person opens the app again (the process was killed) | A checkpoint | The app's cold start for a signed-in user (or the pre-login launch) | The feed, or the login screen when the checkpoint was taken before a session | You come back later and want the account in the state the app starts in |
launch from a snapshot, stored or by hand | The same cold start, for a device and session you carry | A snapshot; each of device, application and session is optional | The same cold start | The feed with a session in the snapshot, the login screen without one | You keep only the session, or bring an account that exists in your own system |
resumeSilently | The app never left: the same process, a moment later | A checkpoint | Nothing | The checkpoint's own screen | A flow continues in another process (a code screen, a form), no time has passed |
resumeFromBackground | The app was in the background and the person returns to it | A checkpoint | The top screen's return visit; the app's other requests on returning from the background are not sent | The checkpoint's own screen | Little time has passed since the checkpoint |
resumeByReentering | The person walks the same path again by tapping | A checkpoint | Every screen's enter flow, from the root to the checkpoint's screen | The checkpoint's own screen with fresh data | Time has passed and the path's data must be current |
A challenge any of these runs into is answered by otherwise and never reaches main (Challenges: answer them once with otherwise).
One short example per row:
ts
import { AndroidIgpapi, FeedTimelineScreen } from '@igpapi/android';
import { Effect } from 'effect';
import { readFile, writeFile } from 'node:fs/promises';
const username = process.env['IG_USERNAME'] ?? '';
const password = process.env['IG_PASSWORD'] ?? '';
// install: a new device, the login screen.
const checkpoint = await AndroidIgpapi.install({
seed: username,
main: home =>
Effect.gen(function* () {
const next = yield* home.logIn({ username, password });
return FeedTimelineScreen.is(next) ? yield* next.checkpoint() : undefined;
}),
});
if (checkpoint === undefined) throw new Error('The login did not reach the feed: see "Log in".');
await writeFile('session.json', JSON.stringify(checkpoint));
// A checkpoint read back from your storage is an object: parse the file.
const stored = JSON.parse(await readFile('session.json', 'utf8'));
// launch from a checkpoint (or a snapshot): the app's cold start, then the feed.
await AndroidIgpapi.launch({
checkpoint: stored,
main: screen => Effect.succeed(FeedTimelineScreen.is(screen) ? screen.items.length : 0),
});
// resumeSilently: continue on the checkpoint's own screen, sending nothing.
await AndroidIgpapi.resumeSilently({
checkpoint: stored,
main: screen => Effect.succeed(screen.id),
});
// resumeFromBackground and resumeByReentering take the same object.
await AndroidIgpapi.resumeFromBackground({
checkpoint: stored,
main: screen => Effect.succeed(screen.id),
});
await AndroidIgpapi.resumeByReentering({
checkpoint: stored,
main: screen => Effect.succeed(screen.id),
});Resume: continue on the checkpoint's own screen
A launch runs the app's cold start, so it shows the screen the app shows first, not the checkpoint's screen. Continuing on the checkpoint's own screen is a resume. A resume needs no layer from you: the checkpoint is the whole client state and the whole process, so the entry builds the process from it and main receives the screen's own handle.
Name the screen you expect and the handle is exactly that screen's, with no narrowing afterwards. A challenge that a resume sending requests (resumeFromBackground, resumeByReentering) runs into is answered by otherwise, as for a launch; a silent resume sends nothing, so it cannot meet one.
ts
import { AccountRecoveryAuthenticationConfirmationScreen, AndroidIgpapi } from '@igpapi/android';
import { Effect } from 'effect';
import { readFile } from 'node:fs/promises';
// The checkpoint taken on the screen that waits for the emailed code, in an earlier process.
const stored = JSON.parse(await readFile('recovery.json', 'utf8'));
// The code arrives by email after that earlier process ended; here it is passed on the command line.
const code = process.argv[2] ?? '';
await AndroidIgpapi.resumeSilently({
checkpoint: stored,
screen: AccountRecoveryAuthenticationConfirmationScreen,
main: screen =>
Effect.gen(function* () {
// Already this screen's handle: no `is` needed.
yield* screen.logInWithCode(code);
}),
});Without the screen field main receives the union of all screen handles, told apart by id, and one XxxScreen.is(screen) narrows it. A checkpoint of another screen than the named one rejects with the typed CheckpointScreenMismatchError before any process is built or any request is sent.
resumeSilentlyputs the process and the open screens back and sends nothing.resumeFromBackgrounddoes the same and runs the top screen's return visit.resumeByReenteringrestores and replays every screen's enter flow from the root, as if the person walked the path again by tapping. A checkpoint on the launch's splash comes back silently, since a splash is part of the launch and not a step the person takes.
A checkpoint resumes only in the release that wrote it. After an upgrade, launch it (Keep a session).
Options and what is fixed
Each entry takes the transport settings of the process it builds as fields of its object (proxy, dangerAcceptInvalidCerts, requestTimeoutMs, protocolMode): they are your environment, not part of a checkpoint (Proxy, network and location). Everything else the process is (the phone, the session, the network, the position) is in the checkpoint, so a resume takes no network or location. A launch from a checkpoint is a new process and does accept network.
The stored forms are exclusive: seed, snapshot and checkpoint cannot be combined, so passing two is a type error rather than a silently preferred value.