Appearance
Two-factor without the screens (SMS, WhatsApp)
What it is for. Finishing a two-factor login for an account that confirms with an SMS or a WhatsApp code. The two-factor screens act only on the authenticator app and on an approval on another device (Two-factor login); for an SMS or a WhatsApp code, you compose the login over the app layer with the effects of this page.
What you call. AndroidLogin (the login over the app layer) fails with LoginTwoFactorRequiredError when the account asks for a second step. The effects of this page finish it: look at the methods the account offers, take one, submit its code. The simpler way for an account you control is to turn on the authenticator app (Turn on the authenticator app) and use the screens.
What you get. The logged-in profile, or a typed error that names what went wrong (the table below).
An example
The program below logs in over the app layer (Compose the app layer yourself) and, when the account asks for a second step, answers it by the method the account offers. The code is read after it exists (Codes that arrive during the run).
ts
import {
AndroidApp,
AndroidLogin,
AndroidTwoFactorGetMethods,
AndroidTwoFactorSelectTotp,
AndroidTwoFactorVerifyCode,
type LoginTwoFactorRequiredError,
totpCodeOf,
TwoFactorBloksSendWhatsAppCode,
} from '@igpapi/android';
import { Data, Effect } from 'effect';
import { createInterface } from 'node:readline/promises';
const username = process.env['IG_USERNAME'] ?? '';
const password = process.env['IG_PASSWORD'] ?? '';
const totpSecret = process.env['IG_TOTP_SECRET'] ?? '';
/** The code did not arrive: nobody typed it, or your system did not answer. */
class CodeUnavailableError extends Data.TaggedError('CodeUnavailableError')<{
readonly kind: 'sms' | 'whatsapp';
}> {}
/** Waits for a code at the moment it exists ("Codes that arrive during the run"). */
const readCode = (kind: 'sms' | 'whatsapp') =>
Effect.tryPromise({
try: async () => {
const terminal = createInterface({ input: process.stdin, output: process.stdout });
try {
return (await terminal.question(`The ${kind} code: `)).trim();
} finally {
terminal.close();
}
},
catch: () => new CodeUnavailableError({ kind }),
}).pipe(Effect.timeout('5 minutes'));
// Finishes the login the account's second step asks for: look at what the account offers, take one, submit its code.
const finishTwoFactor = (error: LoginTwoFactorRequiredError) =>
Effect.gen(function* () {
const twoStepVerificationContext = error.challengeContext;
const picker = yield* AndroidTwoFactorGetMethods({
twoStepVerificationContext,
screenId: error.methodPickerScreenId,
});
if (picker.methods.includes('whatsapp')) {
// This sends the code by WhatsApp; the code is read after it exists.
const { maskedContactPoint } = yield* TwoFactorBloksSendWhatsAppCode({ twoStepVerificationContext });
const code = yield* readCode('whatsapp');
return yield* AndroidTwoFactorVerifyCode({
twoStepVerificationContext,
code,
challenge: 'whatsapp',
maskedContactPoint: maskedContactPoint ?? undefined,
});
}
if (picker.methods.includes('sms')) {
// The server sent the SMS; the code is read after it exists.
const code = yield* readCode('sms');
return yield* AndroidTwoFactorVerifyCode({ twoStepVerificationContext, code, challenge: 'sms' });
}
// The authenticator app: the code is computed where it is sent.
const screen = yield* AndroidTwoFactorSelectTotp({ twoStepVerificationContext, selectQpl: picker.selectQpl });
return yield* AndroidTwoFactorVerifyCode({
twoStepVerificationContext,
code: totpCodeOf(totpSecret),
challenge: 'totp',
qplMarkerId: screen.verifyQpl?.markerId,
qplInstanceId: screen.verifyQpl?.instanceId,
});
});
// A login that needs no second step finishes by itself; otherwise the second step runs.
const program = AndroidLogin({ username, password }).pipe(
Effect.catchTag('LoginTwoFactorRequiredError', finishTwoFactor),
);
await Effect.runPromise(Effect.provide(program, AndroidApp({ seed: username })));The error and what it carries
LoginTwoFactorRequiredError carries:
challengeContext: a token every following call passes back astwoStepVerificationContext;resolution: what the server chose first, eitherapprove_from_another_device(the login waits for an approval on another device) orchoose_method(the server's default is another method, andmethodslists the methods on offer);methodPickerScreenId: the id of the "Try another way" picker, whichAndroidTwoFactorGetMethodstakes asscreenId.
An empty or unreadable answer to the second-step request is a separate error, LoginTwoFactorEntrypointParseError, not LoginTwoFactorRequiredError: a program that handles one handles the other too.
The effects
| Effect | What it does |
|---|---|
AndroidTwoFactorGetMethods | Reads the methods the account offers (approve_from_another_device, whatsapp, totp, sms, backup_codes). |
AndroidTwoFactorSelectTotp | Chooses the authenticator app and opens its code screen. |
TwoFactorBloksSendWhatsAppCode | Makes Instagram send the WhatsApp code and answers the masked phone number it went to. |
AndroidTwoFactorVerifyCode | Submits the code (challenge: 'totp', 'whatsapp' or 'sms') and answers the logged-in profile. |
AndroidTwoFactorPollTrustedNotification | Asks once whether the approval on another device has come: pending, approved (with the profile) or denied. |
The Simulator never polls by itself: for an approval, call AndroidTwoFactorPollTrustedNotification about every 7.5 seconds, as the app does, and bound the wait.
Errors
| Tag | What it means | What to do |
|---|---|---|
LoginTwoFactorRequiredError | The account asks for a second step. | Finish it with the effects above. |
LoginTwoFactorEntrypointParseError | The second-step answer was empty or unreadable. | Retry; report it if it repeats. |
TwoFactorBloksInvalidCodeError | The code was wrong, expired, or refused for too many attempts. The reasons are not told apart. | Ask for the code again. |
TwoFactorBloksLoginResponseNotFoundError | The answer to the code carried no login and no rejection. | Retry; report it if it repeats. |
TwoFactorBloksIssuedIdMissingError | A value the previous answer must issue was missing. | Start the login again. |
Every error has a row in Errors.