Skip to content

Sign in as an emulated Mac: Apple's macOS CoreADI under Unicorn, on the phone - #274

Open
lazzurs wants to merge 9 commits into
parawanderer:mainfrom
lazzurs:fix/248-sign-in-as-an-emulated-mac
Open

lazzurs wants to merge 9 commits into
parawanderer:mainfrom
lazzurs:fix/248-sign-in-as-an-emulated-mac

Conversation

@lazzurs

@lazzurs lazzurs commented Oct 21, 2026 •

Copy link
Copy Markdown

Fixes #248.

Since 2026-10-12 Apple only issues the Find My token (searchPartyToken) to a sign-in whose Anisette machine was minted by a Mac or an iPhone. An Android-minted machine gets mmeAuthToken alone and report fetches return 401 (malmeloo/FindMy.py#278). Since about 2026-10-20 the GSA edge also returns 503 for any X-MMe-Client-Info whose OS token is Android (probe results on #261). So the Android ADI libraries can't reach Find My any more, whatever headers are sent.

This runs Apple's macOS CoreADI on the phone instead, under an x86_64 emulator - the "derived library that can be run on android directly" from #248, built off sacca97/os-findmy-e.

How it works

  • libmacadi.so (app/src/main/cpp/macadi/): a C++ port of os-findmy-e's Rust adi-core - Mach-O loader, Unicorn x86_64 VM with the handful of macOS calls the library makes, and the CoreADI dispatcher framing. Unicorn 2.1.4 is fetched by CMake at build time, pinned by SHA-256, and only linked into this library.
  • The library comes from Apple at runtime, 3.5 MB, one request. It is the x86_64 CoreADI in iTunes 12.8.3 for Mac, which Apple still serves. In that DMG the CoreADI.pkg payload is stored uncompressed and contiguous, so a single HTTP range request gets it. The payload and the extracted library are both pinned by SHA-256. scripts/locate_mac_coreadi.py re-derives the offsets (from Apple, or from a local copy of the DMG) and reads its constants from the Java sources. Nothing of Apple's is in the repo or the APK.
  • AnisetteForSession picks the machine in AppDependencies. A fresh sign-in gets EmulatedMacAnisette. A restored session keeps the machine that signed it in: LocalAnisette for every session it established (and for installs that recorded nothing), and the emulated Mac only for sessions it signed in, restored without ever provisioning. Provenance lives in LocalAnisette's preferences (SessionProvenance). EmulatedMacAnisette provisions one emulated Mac with akd's headers (from os-findmy-e's gsa.py), keeps its identity and ADI store under files/anisette/mac/ for the life of the install, and serves the OTP, machine id and Apple-assigned routing number to the existing Python sign-in.
  • Its own process. libmacadi.so loads only in a non-exported :macadi process (MacAdiService), and the app calls it over binder (RemoteMacAdi). A native crash there reaches the app as MacAdi.EmulatorDiedException and is recorded per app version and library build, the same scheme as TryItElsewhereFirst, rather than making the app unopenable. It also keeps the GPL code out of the app process.
  • Python: hsc 409 with no au on GSA complete is treated as a second factor (the pinned FindMy.py otherwise skips 2FA). The proper fix, _normalize_2fa_status in _gsa_authenticate as in Fix and integrate with anisette mac backend malmeloo/FindMy.py#279, is prepared for parawanderer/FindMy.py; until the pin moves the runtime wrapper stays, and it steps aside once the pinned FindMy.py has the fix; the routing number comes from Java; CloudKit and the keychain session get a view of the account whose device_uuid is a UUID, because a Mac's device id (00006000-…) isn't one and FindMy.py's CloudKit client parses it as one.

Things to decide

  • Licensing (accepted as GPL-2.0, see docs/emulated-mac-emulator-choice.md). The APK carries Unicorn 2.1.4's licence files and a NOTICE.txt with the exact source tarball and SHA-256 under assets/licences/unicorn/, and build-release.yml attaches that verified tarball to each release. The root NOTICE lists Unicorn and os-findmy-e (MIT).
  • The emulated Mac is a made-up Mac. Each install registers one fake MacBook Pro on the user's Apple account (serial 0PENTAGV…, as before). Apple could start fingerprinting this; it has changed the rules four times in five weeks.
  • Run the account's loop on its own thread so iCloud calls stop queueing behind fetches #273 also adds an AGENTS.md rule 21; whichever lands second renumbers.

Testing

  • On a Pixel, with a real account: fresh install → library fetched from Apple → emulated Mac provisioned → sign-in with password and 2FA → tags listed from the iCloud account. Two bugs found that way and fixed here:
    • provisioning leaves the legacy adi.pb in the store, and while it exists the library reports "not provisioned", so the first sign-in after provisioning sent an empty machine id (ec -80009);
    • the CloudKit device id above (badly formed hexadecimal UUID string).
  • CI on the fork (Sign in as an emulated Mac (fork CI run before the upstream PR) lazzurs/OpenTagViewer#2): static checks, JVM tests, Chaquopy bridge tests, debug APK, instrumented suite, native stubs, ADI on bionic, and the new macadi-host.yml, which range-fetches the library from Apple exactly as the app does, checks both hashes, builds the native core for the host and loads the library ("not provisioned").
  • New tests: MacCoreAdiFetcherTest (cpio walk, universal-binary slice, refusing a server that ignores the range), test_emulated_mac.py (409 shim, routing, CloudKit device id view), and on the JVM EmulatedMacAnisetteTest, MacIdentityTest, MacProvisioningTest (against a local FakeGrandSlam socket), RestoringASessionOnTheMachineThatSignedItInTest and UnicornNoticeTest. Instrumented: TheEmulatedMacInItsOwnProcessTest (real CoreADI loads in :macadi, the app process never maps libmacadi.so, a SIGSEGV there arrives as a death).
  • Byte-for-byte against adi-core: scripts/adi-emulator-diff/compare-libmacadi.sh runs the same offline calls through libmacadi.so and os-findmy-e's adi-diff; all 20 lines match, in CI as a second macadi-host.yml job.

Not verified: a device sign-in since the review changes (the Pixel test above predates moving the emulator into :macadi); the byte-for-byte comparison of the OTP and provisioning paths, which needs a provisioned store; how long a provisioned emulated Mac and its token stay accepted over days; more than one account.

Credit to @sacca97 for os-findmy-e, which this is ported from, and to @hoorayman and @poterpan on #248 for establishing that a Mac-minted machine is what gets searchPartyToken.

🤖 Generated with Claude Code

@parawanderer parawanderer left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes. Item 1 blocks the merge; 2–4 are required alongside it.

  1. Existing sessions are moved onto a new machine on upgrade. AppDependencies.java:63 makes EmulatedMacAnisette the source for every caller, so on restore _preferLocalAnisette (main.py:896) calls its ensureReady(), which downloads CoreADI and provisions a new Mac at app start, then swaps it into the restored account. That session's token was issued to the Android machine, and per #248 a token works only with Anisette from the machine that obtained it, so it should start answering 401. identityForRestore (main.py:863) also carries the old Android device id across, so Apple receives the new Mac's X-Apple-I-MD-M alongside the Android X-Mme-Device-Id. Sessions established before 2026-10-12 still fetch reports today; @parawanderer's does. Fix: keep LocalAnisette for any session it established and give the emulated Mac only to fresh sign-ins. A restored session must not provision anything.

  2. libmacadi.so loads in the app process with no crash guard. MacCoreAdi.java:67 calls System.loadLibrary directly, and the catch (Exception | LinkageError) at EmulatedMacAnisette.java:114 cannot catch a SIGSEGV from Unicorn or from Apple's code. That is the #232 failure mode (AGENTS.md rule 4): a native crash on the sign-in screen makes the app unopenable. Run it behind the :adiprobe process / TryItElsewhereFirst and NativeLoadGuard, or host the emulator in its own process and call it over binder. The second option also keeps the GPL code in a separate program from the Apache-2.0 libraries and the proprietary map SDKs (item 4).

  3. Session provenance is lost on upgrade. EmulatedMacAnisette stores sessionEstablishedLocally in a new preferences file, anisette-emulated-mac (line 41), so for every existing session wasSessionEstablishedLocally() reads false and the machine-change warning in _preferLocalAnisette never fires. Read the flag LocalAnisette already wrote. This mostly goes away with item 1.

  4. Unicorn is accepted as GPL-2.0. Add a licence notice for it to the app or the release, and see item 2 for keeping it out of the app process. Decision and reasoning: docs/emulated-mac-emulator-choice.md (#275).

  5. Tests. The pure logic here is untested: EmulatedMacAnisette's state machine (first provisioning, the lost-store branch that registers a new machine, failure falling back to remote, adi.pb never saved), MacIdentity generation and save/load round trip, and the request shapes MacProvisioning sends. Put them on the JVM with the native layer behind an interface and a FakeApkServer-style local socket for provisioning (rule 13). Add one for item 1 as well: a restored session established with LocalAnisette keeps LocalAnisette and never provisions.

  6. The C++ port has no byte-for-byte comparison against os-findmy-e's Rust adi-core, as the PR says. scripts/adi-emulator-diff/ (#275) drives adi-core with a frozen clock and a fixed host UUID; run the same sequence through libmacadi.so and diff. A mismatch on these offline calls is a port bug that a single successful sign-in will not show.

  7. Move the hsc 409 handling into parawanderer/FindMy.py's _gsa_authenticate instead of wrapping _gsa_request at runtime (main.py:343, main.py:898). malmeloo/FindMy.py#279 makes the same change upstream.

Non-blocking: MacBookPro13,2 never shipped macOS 13.1, which is what rule 11 forbids. Keeping the values os-findmy-e was observed to get searchPartyToken with is defensible; note it in MacIdentity alongside the Darwin mismatch already noted there.

Interactively co-authored by Claude Code and @parawanderer

alex-so-3 pushed a commit to alex-so-3/OpenTagViewer-triki that referenced this pull request Oct 25, 2026
…icle diff

parawanderer#274 runs Apple's macOS CoreADI under Unicorn, which is GPL-2.0. The decision is to ship it,
with a licence notice and the library in its own process, and to swap it only if somebody has a
reason to. A swap is invisible to users only if the replacement produces the same bytes, so
scripts/adi-emulator-diff/ puts icicle (MIT OR Apache-2.0) under os-findmy-e's adi-core and diffs
it against Unicorn. Every offline call matched byte for byte; the OTP path, which needs a
provisioned machine, has not been compared.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rob Lazzurs and others added 9 commits November 3, 2026 23:47
A C++ port of sacca97/os-findmy-e's adi-core (MIT, see macadi/NOTICE.md): a
Mach-O loader, a Unicorn x86_64 VM with the few pieces of macOS userland the
library calls, and the CoreADI dispatcher framing. Unicorn 2.1.4 (GPL-2.0) is
fetched at build time, pinned by SHA-256, and linked only into libmacadi.so.

The library itself is never in the repository or the build. In iTunes 12.8.3
for Mac the package holding it is stored uncompressed and contiguous, so it is
one HTTP range request of 3.5 MB; scripts/locate_mac_coreadi.py re-derives the
offsets and checks both pinned hashes, and macadi-host.yml builds the core for
the host and loads the library into it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012JDrrt9ff7WwPepHs6B2Gx
Since 2026-10-12 Apple issues searchPartyToken only to Mac- or iPhone-minted
Anisette, and the edge now refuses any client claiming Android, so the Android
ADI path cannot reach Find My. EmulatedMacAnisette fetches the macOS library
from Apple, provisions one emulated Mac with akd's headers, keeps its identity
and store for the life of the install, and is what AppDependencies hands out.

Also: read Grand Slam's hsc 409 without au as a second factor; send the routing
number Apple assigned at provisioning; give CloudKit a UUID, since a Mac's
device id is not one; drop the legacy adi.pb before every session, which
otherwise made a freshly provisioned machine report unprovisioned (ec -80009).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012JDrrt9ff7WwPepHs6B2Gx
…its own process

A session LocalAnisette signed in keeps LocalAnisette, and nothing is
provisioned to restore it. AppDependencies made EmulatedMacAnisette the
source for every caller, so a restore provisioned a new Mac at app start
and paired its X-Apple-I-MD-M with the account's Android device id. Per
parawanderer#248 that session's token only works with Anisette from the Android
machine, so a working session would start answering 401.

AnisetteForSession now chooses: the emulated Mac for a fresh sign-in,
and for a restore whichever machine signed the session in.
SessionProvenance records that in LocalAnisette's own preferences file,
reading the sessionEstablishedLocally flag every older install already
has and adding sessionMachine=emulatedMac beside it for the Mac's
sessions. The separate anisette-emulated-mac file is gone, so the
machine-change warning works again for upgraders. A Mac session is
restored as Purpose.RESTORE, which never creates or provisions a
machine: a lost store falls back to remote with the warning.

libmacadi.so now loads only in a :macadi process. MacAdiService hosts
the emulator and RemoteMacAdi calls it over binder, so a SIGSEGV from
Unicorn or Apple's code kills that process rather than the sign-in
screen (rule 4, parawanderer#232), and the GPL-2.0 emulator is a separate program
from the rest of the APK. A death during a call is recorded per app
version and library build, and the Mac is not tried again until one of
them changes. OpenAirTagApplication skips its start-up in that process.

The native layer is behind MacAdi, so the logic is tested on the JVM:
EmulatedMacAnisette's state machine, MacIdentity's shapes and round
trip, MacProvisioning's requests against a local FakeGrandSlam socket,
and the restore choice for an install with only LocalAnisette's flag.
TheEmulatedMacInItsOwnProcessTest checks the process boundary on a
device, including that the app process never maps libmacadi.so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015MJtmGkNZpfzFGSN1BZ82a
…ease

libmacadi.so links Unicorn 2.1.4, which is GPL-2.0. The APK now carries
Unicorn's own licence files (COPYING, its QEMU licence, LGPL2, GLIB)
under assets/licences/unicorn/, with a NOTICE.txt naming the exact
source tarball and hash and saying the code runs in its own process.
UnicornNoticeTest fails if that notice stops matching MacAdi.cmake.

The source is fetched by FetchContent rather than being in the tag, so
build-release.yml now downloads the same tarball, checks it against the
pinned hash, and attaches it to the release beside the APK. The root
NOTICE gains Unicorn and os-findmy-e entries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015MJtmGkNZpfzFGSN1BZ82a
compare-libmacadi.sh builds adi-core's adi-diff (the driver run.sh
uses for Unicorn against icicle) and the port's new macadi_diff, runs
both over the same offline CoreADI calls with a frozen clock and a
fixed host UUID, and diffs the output. macadi-host.yml runs it on every
change to the port.

To make that possible CoreAdi exposes the raw calls adi-core has
(begin_session, IsMachineProvisioned, RequestOTP, GetIDMSRouting,
ProvisioningStart) and the VM's files. headers() and
provisioningBegin() are now built from those same calls, so the
comparison covers the code the app runs.

Result on x86_64 Linux: all 20 lines identical, with the port on
Unicorn 2.1.4 and adi-core on unicorn-engine 2.1.5. Checked for teeth by
breaking vm.cpp on purpose: a stray byte in created files turns six
lines red, while changing arc4random_buf or truncating write changes
nothing, because the offline calls never reach them. Both drivers take
an optional provisioned store for the OTP path, which has not been run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015MJtmGkNZpfzFGSN1BZ82a
The fix belongs in FindMy.py's _gsa_authenticate, where
malmeloo/FindMy.py#279 adds _normalize_2fa_status; the same change is
prepared for parawanderer/FindMy.py. Until the pin moves,
_treatHsc409AsSecondFactor keeps wrapping _gsa_request, but returns
without touching the account when the pinned FindMy.py has the
function. test_the_pinned_findmy_does_not_yet fails on the pin move,
which is the cue to delete the wrapper.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015MJtmGkNZpfzFGSN1BZ82a
The 2016 13-inch MacBook Pro stops at macOS 12, so the pair is not a
real release, which rule 11 asks for. These are the values os-findmy-e
was observed to get searchPartyToken with, and changing either is a new
machine to Apple, so they stay until a consistent pair is shown to work.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015MJtmGkNZpfzFGSN1BZ82a
Records what the review taught: never move an existing session onto the
emulated Mac, where provenance lives, why libmacadi.so loads only in
:macadi, the GPL obligations and where each is met, the byte-for-byte
check, and that the hsc 409 wrapper goes when the FindMy.py pin moves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015MJtmGkNZpfzFGSN1BZ82a
@lazzurs
lazzurs force-pushed the fix/248-sign-in-as-an-emulated-mac branch from d9e695c to 6100a00 Compare November 4, 2026 00:46
@lazzurs

lazzurs commented Nov 4, 2026

Copy link
Copy Markdown
Author

Thanks for the thorough review. I've rebased onto main (after #275 and #278) and pushed the fixes as separate commits. Item by item:

1. Restored sessions keep LocalAnisette and never provision. AppDependencies now hands out AnisetteForSession::create instead of EmulatedMacAnisette::new. A fresh sign-in (the login screen, hasExistingSession = false) gets the emulated Mac. A restore gets whichever machine signed the session in: LocalAnisette for every session it established, and for every install that has recorded nothing, so a pre-2026-10-12 session restores exactly as it did before this PR. Only a session the emulated Mac signed in is restored onto it, as EmulatedMacAnisette.Purpose.RESTORE, which never creates or provisions a machine. If that Mac's store has gone, it falls back to remote with the machine-change warning rather than registering a new Mac. Because a LocalAnisette session never reaches the Mac now, identityForRestore can no longer pair the Mac's X-Apple-I-MD-M with the Android X-Mme-Device-Id.

2. Native-crash guard. I took the separate-process option. libmacadi.so now loads only in a :macadi process (MacAdiService, not exported), and the app talks to it over binder through RemoteMacAdi. Nothing in the app process touches MacCoreAdi, and OpenAirTagApplication returns early in :macadi, as it does in :adiprobe. If that process dies during a call, the call throws MacAdi.EmulatorDiedException, the death is recorded per app version and library build (the same scheme as TryItElsewhereFirst), and the Mac isn't tried again until one of those changes. A process that went away between calls is reopened rather than written off. TheEmulatedMacInItsOwnProcessTest (instrumented) covers three things. Apple's real CoreADI loads over there and answers "not provisioned". The app process never maps libmacadi.so. A real SIGSEGV over there reaches the app as a death, and the emulator starts again afterwards.

3. Provenance. The anisette-emulated-mac preferences file is gone. SessionProvenance reads and writes sessionEstablishedLocally in LocalAnisette.PREFERENCES, and the emulated Mac adds one key beside it, sessionMachine = emulatedMac. Existing sessions read as local again, so the warning in _preferLocalAnisette fires as it should.

4. GPL-2.0 notice. The APK now carries Unicorn's own licence files from the 2.1.4 tag under assets/licences/unicorn/: COPYING, the QEMU LICENSE, COPYING.LGPL2 and COPYING_GLIB. Next to them is a NOTICE.txt naming the exact source tarball and its SHA-256. build-release.yml downloads that tarball, checks it against the hash in MacAdi.cmake, and attaches it to the release beside the APK, since FetchContent means it isn't in the tag. The root NOTICE has Unicorn and os-findmy-e entries. UnicornNoticeTest fails if the notice stops matching MacAdi.cmake. Together with item 2, that covers both obligations in docs/emulated-mac-emulator-choice.md.

5. JVM tests. The native layer is behind a MacAdi interface, and Base64 and plist parsing are injectable, so all of this runs on the JVM:

  • EmulatedMacAnisetteTest (22): first provisioning, the next launch reusing the machine, a lost store registering a new machine on sign-in, and the same lost store registering nothing on restore. It also covers restore with no identity or an unprovisioned one, failures falling back to remote and being retried, adi.pb never reaching disk, emulator death being recorded and not retried, and provenance.
  • MacIdentityTest (9): generated shapes, the save/load round trip, and an unreadable file being an error rather than a fresh machine.
  • MacProvisioningTest (9): the three requests against FakeGrandSlam, a FakeApkServer-style socket on 127.0.0.1. It checks every header, both bodies, that Apple's answers reach the library, and the 433, missing-endpoint, missing-tk and bad-routing cases.
  • RestoringASessionOnTheMachineThatSignedItInTest (7) is the test for item 1. An install with only LocalAnisette's flag on disk, true or false, or nothing at all, keeps LocalAnisette. Only a Mac-recorded session goes to the Mac.

I made sure they can fail. Reverting the restore choice, the restore-never-provisions guard, the adi.pb skip or one provisioning header turns nine tests red.

6. Byte-for-byte comparison. scripts/adi-emulator-diff/compare-libmacadi.sh builds adi-core's adi-diff and a new C++ driver, app/src/test/macadi-host/macadi_diff.cpp. The driver makes the same calls through the port and prints the same lines. The script diffs the two outputs. CoreAdi now exposes the raw calls adi-core has, and headers() and provisioningBegin() are built from them, so the diff covers the code the app runs. macadi-host.yml runs it as a second job. Result on x86_64 Linux: all 20 lines identical, with the port on Unicorn 2.1.4 and adi-core on the unicorn-engine 2.1.5 crate. To check the diff has teeth, I broke vm.cpp on purpose. A stray byte in created files turns six lines red. Changing arc4random_buf's output or truncating write changes nothing, because the offline calls never reach them. So this covers the loader, the dispatcher, request framing and Initialize's file paths, but not the OTP path. Both drivers accept an optional provisioned store for that. It hasn't been run yet, because it needs a copy of a provisioned store. I've written up the extent in the emulator-choice doc.

7. hsc 409 in FindMy.py. I've prepared the change for parawanderer/FindMy.py, and will open it there as a PR if you're happy with this shape: _normalize_2fa_status, called on the complete response in _gsa_authenticate, with the same name and placement as malmeloo/FindMy.py#279. The tests run the whole SRP exchange offline against a real verifier, so they exercise _gsa_authenticate's own decision. The full suite passes, and so do the app's bridge tests against it. The app pins FindMy.py by commit in four places (rule 14), so it can't use the fix until that's merged. Until then _treatHsc409AsSecondFactor stays, but it returns without touching the account once the pinned FindMy.py has _normalize_2fa_status. A test fails when the pin moves, as a reminder to delete it. Once the FindMy.py PR is merged, I'll bump the pin and remove the wrapper in this PR.

Non-blocking. I've added a note to MacIdentity. MacBookPro13,2 stops at macOS 12, so it never ran 13.1. These are the values os-findmy-e was observed to get searchPartyToken with, and changing either would be a new machine to Apple. I've kept them until a consistent pair is shown to work.

AGENTS.md rule 21 is updated with the above. Upstream's numbering only goes to 20, so it stays 21.

On my fork's CI, the JVM suite, the bridge tests, the debug APK build (including the NDK build of libmacadi.so), static checks and both macadi-host.yml jobs pass. In the emulator suite, all three TheEmulatedMacInItsOwnProcessTest tests pass. The only failures were the two DebugModeReachesEveryScreenTest history checks. They ran at 00:39 UTC, and AMapWithTagsOnIt dates its report two hours back, which lands on the previous day before 02:00. That's unrelated to this PR.

What I haven't verified: I haven't done a sign-in on a phone since these changes. The emulator suite swaps Anisette for a fake, so a real sign-in through :macadi still needs a device, and I'll do one before asking for another look.

@lazzurs

lazzurs commented Nov 4, 2026

Copy link
Copy Markdown
Author

I've opened the FindMy.py change for item 7 as parawanderer/FindMy.py#9. Once that's merged I'll move the pin in this PR and delete _treatHsc409AsSecondFactor.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

App: Can't log in

2 participants