Appearance
Create an account
Registration is the screens a person walks, in the app's order. This page is one program that walks them all and answers every error.
- What the person does. The login screen's "Create new account" opens the phone-number screen, each screen's action submits what the person typed and opens the next one, and the terms and policies screen's
agree()creates the account. - The SMS code arrives after the phone number is submitted, so the program reads it between
submitPhoneNumberandsubmitConfirmationCode, when it exists (Codes that arrive during the run). - What you get. The server's decision (below). Your program turns a refusal into a value, as
Refused. - Errors. Every error the chain can raise is answered in the example. A challenge is not among them: one raised over a sign-up step is
otherwise's or rejects the call, nevermain's channel. The exhaustiveness check at the end of the example fails the build if a step ever gains an error the example misses.
Each screen, with its actions and errors, is on the registration page.
ts
// The registration example "Create an account" shows, typechecked as written.
import {
type AndroidAppLaunchLoginScreen,
AndroidIgpapi,
type AndroidMainError,
type RegistrationBirthdayInput,
type RegistrationOutcome,
} from '@igpapi/android';
import { Data, Effect } from 'effect';
import { createInterface } from 'node:readline/promises';
/** What the person registers with: what each screen asks them to type. */
interface RegistrationInput {
readonly phoneNumber: string;
readonly password: string;
readonly birthday: RegistrationBirthdayInput;
readonly fullName: string;
readonly username: string;
}
/** The SMS code did not arrive: nobody typed it, or your system did not answer. */
class CodeUnavailableError extends Data.TaggedError('CodeUnavailableError')<{
readonly kind: 'sms';
}> {}
/**
* Waits for the SMS code at the moment it exists ("Codes that arrive during the run"). Here a person types it
* at the terminal; your program can read an SMS provider instead.
*/
const readCode = (kind: 'sms') =>
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'));
/**
* A fresh-install Android process registers a new account the way a person
* does: the launch shows the login screen, its "Create new account" opens the
* first registration screen, and each screen's action submits what the person
* typed and opens the next one. The terms and policies screen's `agree()`
* creates the account and answers the server's decision.
*/
const register = (home: AndroidAppLaunchLoginScreen, input: RegistrationInput) =>
Effect.gen(function* () {
// A fresh install shows the login screen.
const phoneNumber = yield* home.createNewAccount();
const confirmationCode = yield* phoneNumber.submitPhoneNumber(input.phoneNumber);
// Submitting the number makes Instagram send the SMS, so the code is read now, after it exists.
const smsCode = yield* readCode('sms');
const password = yield* confirmationCode.submitConfirmationCode(smsCode);
const birthday = yield* password.submitPassword(input.password);
const name = yield* birthday.submitBirthday(input.birthday);
const username = yield* name.submitName(input.fullName);
const terms = yield* username.submitUsername(input.username);
return yield* terms.agree();
});
/** A refusal turned into a value: the step it happened on, and Instagram's own words. */
interface RegistrationRefusal {
readonly _tag: 'Refused';
readonly step: string;
readonly detail: string | null;
}
/** Answers an error with a refusal; the handlers below are Effects. */
const stop =
(step: string) =>
(detail: string | null = null): RegistrationRefusal => ({ _tag: 'Refused', step, detail });
/**
* The registered account, or a refusal. Every error the chain can raise is
* answered here — the person's input step by step, what Instagram decided about
* the account, the transport interrupting any request, and the process's own
* failures. The exhaustiveness check at the end of this file pins that the list is
* complete.
*/
const registerOrExplain = (home: AndroidAppLaunchLoginScreen, input: RegistrationInput) =>
register(home, input).pipe(
Effect.catchTags({
// ── The person's input, step by step ──────────────────────────────────
// The number is not one Instagram accepts for a new account.
RegistrationInvalidPhoneError: error => Effect.succeed(stop('phone')(error.errorMessage)),
// The code from the SMS is wrong, expired or too short.
RegistrationSmsCodeError: error => Effect.succeed(stop('code')(error.errorMessage)),
// The password is too short or too weak.
RegistrationWeakPasswordError: error => Effect.succeed(stop('password')(error.errorMessage)),
// The birthday is invalid, or the person is under 18.
RegistrationInvalidBirthdayError: error => Effect.succeed(stop('birthday')(error.errorMessage)),
// The full name is too long or not accepted.
RegistrationInvalidNameError: error => Effect.succeed(stop('name')(error.errorMessage)),
// The username is taken or not allowed.
RegistrationUsernameTakenError: error => Effect.succeed(stop('username')(error.errorMessage)),
// Instagram refused to create the account.
RegistrationCreateAccountError: error => Effect.succeed(stop('account')(error.errorMessage)),
// The request cannot be built: an earlier step's answer was never accepted in this process.
RegistrationStateMissingError: error => Effect.succeed(stop('state')(error.what)),
// The session holds no password-encryption key, so the password cannot be sent.
PasswordEncryptionKeysUnavailableError: () =>
Effect.succeed(stop('password')('the device has no password-encryption key')),
// ── What Instagram decided about the account ──────────────────────────
// Instagram flagged the sign-up as spam and stopped the flow.
RegistrationSpamError: () => Effect.succeed(stop('spam')('Instagram flagged the sign-up as spam.')),
// The account exists, but the launch after it failed. The terms screen
// is still on top: call its `retryLaunch()`, never register again.
RegisteredButLaunchFailedError: error =>
Effect.succeed(
stop('launch')(`the account ${error.account.username} exists; retry the launch on the terms screen`),
),
// The SMS code did not arrive in time.
CodeUnavailableError: () => Effect.succeed(stop('code')('The SMS code did not arrive.')),
TimeoutError: () => Effect.succeed(stop('code')('The SMS code did not arrive in time.')),
// ── The transport interrupting any request ────────────────────────────
// A step's answer asked for an account-verification challenge mid-flow.
RegistrationChallengeRequiredError: () =>
Effect.succeed(stop('challenge')('Instagram asks for an account-verification challenge before continuing.')),
// A two-factor or wrong-code challenge interrupted the flow.
IgLoginTwoFactorRequiredError: () => Effect.succeed(stop('challenge')('Instagram asks for a code.')),
// Instagram blocked the device, the address or the sign-up itself.
IgSentryBlockError: () => Effect.succeed(stop('blocked')('Instagram refused the request from this device.')),
IgIpBlockError: () => Effect.succeed(stop('blocked')('Instagram refused the request from this address.')),
// Instagram limited the traffic or the account.
IgRateLimitError: () => Effect.succeed(stop('limited')('Instagram rate-limited the device.')),
IgBloks429Error: () => Effect.succeed(stop('limited')('Instagram throttled the sign-up screens.')),
IgSpamError: () => Effect.succeed(stop('spam')('Instagram flagged the traffic as spam.')),
// Instagram suspended, disabled or deactivated the account.
IgInactiveUserError: () => Effect.succeed(stop('inactive')('The account is inactive; a challenge is required.')),
IgAccountSuspendedError: () => Effect.succeed(stop('suspended')('The account was suspended.')),
IgAccountDisabledError: () => Effect.succeed(stop('disabled')('The account was disabled.')),
IgPrivateUserError: () => Effect.succeed(stop('account')('The account is private.')),
IgNotFoundError: () => Effect.succeed(stop('request')('Instagram found no such resource.')),
IgLoginRequiredError: () => Effect.succeed(stop('session')('Instagram asks for a login.')),
IgUserLoggedOutError: () => Effect.succeed(stop('session')('The session was logged out.')),
IgCookieConsentRequiredError: () => Effect.succeed(stop('consent')('Instagram asks for cookie consent.')),
// The network or the answer itself: a retry may succeed.
TransportError: error => Effect.succeed(stop('network')(error.message)),
ParseError: error => Effect.succeed(stop('answer')(error.message)),
RequestError: error => Effect.succeed(stop('request')(`HTTP ${error.status}`)),
// ── The screen runtime ────────────────────────────────────────────────
ScreenEnterConcurrentError: () => Effect.succeed(stop('screen')('Another screen transition was running.')),
ScreenEdgeInputError: () => Effect.succeed(stop('screen')('The next screen refused the input.')),
ScreenDataError: error => Effect.succeed(stop('screen')(`The screen answer drifted: ${error.detail}`)),
}),
);
// ─── Exhaustiveness ─────────────────────────────────────────────────────────
/**
* The error channel after the handlers above. It must be `never`: a `false`
* here means an error the example does not answer yet, and the example cannot
* silently miss one that a later change adds to a step. `AndroidMainError` is
* what a screen action can still fail with — everything but the challenge,
* which `otherwise` answers before `main` could see it.
*/
type NoUnhandledErrors<E> = [E] extends [never] ? true : false;
export const everyErrorIsAnswered: NoUnhandledErrors<
AndroidMainError<Effect.Error<ReturnType<typeof registerOrExplain>>>
> = true;
// ─── The run the README shows ───────────────────────────────────────────────
/** What the caller gets: the account outcome, or a refusal to explain. */
type RegistrationAnswer = RegistrationOutcome | RegistrationRefusal;
const input: RegistrationInput = {
phoneNumber: '+15550000001',
password: 'SecureP@ss1',
birthday: { day: 14, month: 6, year: 1997 },
fullName: 'Jessica Miller',
username: 'jessica.miller.72869',
};
// One call is one app process: it launches the app, hands the program the
// login screen, and closes the process when the program ends. This example
// passes no `otherwise`: a challenge the server raises over a sign-up step
// rejects the call with the typed `UnexpectedScreenError` (or the challenge's
// own error), never reaches `main` — describe its branches with
// `defineOtherwise` to answer it (see "Answer a challenge with `otherwise`").
const answer: RegistrationAnswer = await AndroidIgpapi.install({
seed: 'new-account',
main: home => registerOrExplain(home, input),
});
switch (answer._tag) {
case 'Created':
// The account is active: the process continued into the app's launch.
console.log('registered, first screen:', answer.firstScreen.id);
break;
case 'ChallengeRequired':
// The account exists but is not active until the person passes Instagram's verification challenge.
console.log('registered, challenge required:', answer.account.username);
break;
case 'PartiallyCreated':
// The server issued no completion state: follow the challenge branch.
console.log('registered partially:', answer.username);
break;
case 'Refused':
console.log('registration stopped:', answer.step, answer.detail);
break;
}
export { answer, register, registerOrExplain, type RegistrationInput, type RegistrationRefusal };What agree() answers
| Outcome | What it means | What to do |
|---|---|---|
Created | The account is active. The same process continued into the app's launch, and firstScreen is the screen it left on top (the feed). | Carry on with the screen, and save a checkpoint (Keep a session). |
ChallengeRequired | The account exists but is not active until the person passes Instagram's account-verification challenge. The process is not launched and holds no session. | Log in to the new account with install as usual. If Instagram still asks for the verification, your otherwise branches answer it (Challenges). Do not register again. |
PartiallyCreated | The account exists, but Instagram issued no session and no state to complete it with. | Log in to the account with install and its username and password. |
When the answer is partial and carries what a completion needs, agree() completes the account itself and answers that step's outcome. A launch failure after the account exists is RegisteredButLaunchFailedError: the account is created on the server, so retry the launch with the terms screen's retryLaunch(), never the registration, which would create a second account.
Tips
- Phone format: include the country code (
+1…); the Simulator does not prepend it. - Password: the Simulator encrypts it with the session's key before it is sent.
- Birthday: only the 18-and-over flow; an under-age birthday is
RegistrationInvalidBirthdayError. - Username: the Simulator does not check availability; a taken username is
RegistrationUsernameTakenError. - The flow's state lives in the process; you pass only what the person types. A fresh
createNewAccount()starts the flow again.