Background
A partner education portal that runs college-entrance-exam courses decided to use MeetMate as its classroom. The web already had the portal-specific features (live tests, mock exams, score reports, 1:1 live queue), but the mobile app had none of them. When a student tapped a lecture in the portal, the app made them start over from the login screen.
There were two more requirements. Phone screen recording had to be blocked so course materials wouldn’t leak, while still keeping the ability for instructors to share their screen from the app. The problem was that device recording and in-app screen sharing use the same capture path at the OS level.
Approach
First, I built the entry path. Portal deep links are strictly validated: links that aren’t ours are ignored, and links whose contents don’t match show a failure screen. The external session is injected only into memory, not into persistent storage. The existing regular login is left as is and restored from a snapshot when the session ends. Even a 401 in the external session doesn’t wipe the regular login. On cold start, the initial URL is part of the app-ready condition, and links that arrive while already in a meeting are ignored.
Tests and mock exams carry over the web contract as is, with the rules split out into pure functions. Rules like sheet merging, elective subjects, the submission payload, and answer keys were made testable without UI. The 60-second autosave, per-question timers (which start when a question becomes visible, pause in the background, and resume on return), and auto-submit on expiry live in the service layer. At first I put the expiry watcher in the sheet component, but when time ran out with the sheet collapsed, the timer froze at 00:00:00. So I moved the watcher to the service lifetime.
I initially drew the score report as a native table. But following the decision that “it has to be the same file as the web one,” I moved the web jsPDF layout over without touching it. I polyfilled atob/btoa, which Hermes lacks, and the font is downloaded once and cached. After verifying byte equivalence with the same real data, I deleted the native table.
From the OS’s point of view, blocking screen recording and screen sharing are the same act. So instead of designing them separately, I made whether to block decided in one place.
Recording blocking relied on different tools on each platform. Android sets FLAG_SECURE on the window, but because the flag is per-window, it has to be set again whenever the activity is recreated, as on rotation. Miss that, and simply rotating the screen once opens a hole. iOS has no API to block recording, so the app covers the entire screen whenever capture is detected. The final blocking decision is computed in one place — “blocked by policy and I’m not sharing” — and it’s lifted just before sharing starts and reapplied when sharing ends or fails. I measured the share of black pixels at each stage with emulator screencap: normal 0%, blocked 99.7%, while I’m sharing 40.5%, after sharing ends 99.7%, after the session ends 0%.
I cleaned up the remaining gaps too. The screen sharing quality setting, which was saved but whose actual output was fixed at 5fps, is now wired to the actual output cap. Instructor and TA self-views were excluded from mirroring so writing on the board doesn’t appear flipped, and I added a watermark to the instructor tile in the student view. Portal branching wasn’t scattered across components but consolidated into a policy field and a single pure function. When regular meetings also needed recording blocking a week later, I could extend it just by changing the policy value from on/off to a scope (off, during meetings, always). Regular meetings measured lobby 0%, device check 100%, meeting room 99.7%, lobby after leaving 0%.
Results
- A portal deep link goes straight to the device check screen, without login or the lobby. External session separated from the regular login (66 tests).
- Ported live tests, surveys, file attachments, dedicated mock exam screens, and the 1:1 live queue to native, and verified every student and instructor flow on devices.
- Score report PDF byte-identical to the web generator.
- Device recording blocking: 0% → 99.7% black pixels, with in-app screen sharing preserved. Extended to regular meetings.
- Screen sharing quality 5/15/30fps actually applied, self-view mirroring exception, leak-tracing watermark on the instructor tile.
- Tests 1,883 → 2,030, 0 new tsc errors.
What I Learned
When the result has to be identical, don’t rebuild it — move it over as is. While I was redrawing the score report as a native table, differences from the web kept appearing; only after moving the web generator over unchanged could I prove “it’s the same as web” with a byte-comparison test.
Also, consolidating the partner-specific requirements into a single policy field, rather than scattering them as component branches, paid off a week later. When regular meetings also needed recording blocking, it was done just by widening the range of the policy value, without building a new feature.