Appearance
Compose the app layer yourself
Most programs start with AndroidIgpapi.install or launch and never touch the app layer. Choose the layer when you need a request the Simulator has no screen or effect for, or when you want to run several programs over one app process. For a single effect that has no screen (a setting, an authenticator key, a list), call it inside main instead (Effects inside main).
What you give up
The layer path has no screens, no otherwise and no automatic launch. You log in and manage the process yourself, challenges reach you as errors, and the Simulator's pacing and ordering of a screen's requests are yours to keep. That is why the entries are the recommended way.
The layer
AndroidApp(options) is the Instagram Android app on a virtual phone, as an Effect Layer: it provides everything the package's effects run on.
One build of the layer is one app process. Provide it once around the whole program. To run several programs in the same process, keep the layer in a ManagedRuntime you own and dispose it when the process ends.
Log in over the layer
AndroidLogin is the layer path's one-call login. It runs the platform's pre-login steps and then the login request; do not run the pre-login steps by hand before it. (A process started by AndroidIgpapi.install logs in through its login screen instead, because the entry owns its layer and does not hand it out.) ExportSnapshot carries the session out of the program.
ts
import { AndroidApp, AndroidLogin, ExportSnapshot, UsersInfo } from '@igpapi/android';
import { Effect, ManagedRuntime } from 'effect';
import { writeFile } from 'node:fs/promises';
const username = process.env['IG_USERNAME'] ?? '';
const password = process.env['IG_PASSWORD'] ?? '';
// One build of the layer is one app process: keep it, run several programs over it, and dispose it when
// the process ends.
const app = ManagedRuntime.make(AndroidApp({ seed: username }));
// The layer path's login: it runs the pre-login steps itself, then the login request.
const login = AndroidLogin({ username, password }).pipe(
// An account with a second step fails here: see "Two-factor without the screens (SMS, WhatsApp)".
Effect.catchTag('LoginTwoFactorRequiredError', () => Effect.succeed(undefined)),
);
await app.runPromise(login);
// Any effect of the package runs over the same process.
const profile = await app.runPromise(UsersInfo({ id: '123456' }));
console.log(profile.username);
// The session leaves the program as a snapshot (a feed's checkpoint is the screens' way).
const snapshot = await app.runPromise(ExportSnapshot);
await writeFile('snapshot.json', JSON.stringify(snapshot));
await app.dispose();An account with a second step fails the login with LoginTwoFactorRequiredError; finish it with Two-factor without the screens (SMS, WhatsApp).
Lower-level requests
The layer also provides the request services the Simulator's own effects are built on, for an endpoint the Simulator has no effect for. PlainJsonApiHttp sends the request as the app would and answers the parsed JSON body; it fails with the errors of the request (Errors). JsonResponseHttp answers the whole response, with the protocol it came over (Connection protocol).
ts
import { AndroidApp, PlainJsonApiHttp } from '@igpapi/android';
import { Effect } from 'effect';
import { readFile } from 'node:fs/promises';
// A snapshot you exported with `ExportSnapshot` ("Keep a session").
const snapshot = JSON.parse(await readFile('snapshot.json', 'utf8'));
// `PlainJsonApiHttp` sends a request as the app would and answers the parsed JSON body.
// It fails with the errors of the request: see "Errors".
const program = Effect.gen(function* () {
const api = yield* PlainJsonApiHttp;
return yield* api({ url: 'api/v1/users/123456/info/' });
});
const result = await Effect.runPromise(Effect.provide(program, AndroidApp({ snapshot })));
console.log(result);Challenges over the layer
There is no otherwise over the layer: a request that meets a challenge fails with IgCheckpointError. Catch it once and open the challenge as screens of the process it interrupted with OpenChallenge(error). The first step is a screen; narrow it with its guard and answer it with the screen's actions (Challenges). OpenChallenge never retries the request: take it again yourself.
ts
import { AndroidApp, ChallengeVerifyCodeScreen, OpenChallenge, UsersInfo } from '@igpapi/android';
import { Effect } from 'effect';
import { readFile } from 'node:fs/promises';
// A snapshot you exported with `ExportSnapshot` ("Keep a session").
const snapshot = JSON.parse(await readFile('snapshot.json', 'utf8'));
// Over the app layer there is no `otherwise`: you catch the challenge once, and open it as screens of the
// process it interrupted. The error carries that process, so no layer is provided for it.
const program = UsersInfo({ id: '123456' }).pipe(
Effect.catchTag('IgCheckpointError', error =>
Effect.gen(function* () {
const outcome = yield* OpenChallenge(error);
// The first step of the challenge is a screen, narrowed with its own guard.
if (ChallengeVerifyCodeScreen.is(outcome)) return 'the challenge asks for a code';
return 'another challenge step';
}),
),
);
const result = await Effect.runPromise(Effect.provide(program, AndroidApp({ snapshot })));
console.log(result);