Files
CBS-Mobile-App/download-fonts.js
T
Abin ToppoandClaude Opus 5.5 f960a119e8 Add Positive Pay, statement guide v1.0, onboarding, and switch every feature on
- Positive Pay on the 12 Oct /pps contract (account number, not account ID):
  OTP enrollment, cheque submit, saved cheques, detail and stop payment.
  Full flow passed on the phone against mobile-banking-backend.lincpay.
- Statement & Mini Statement guide v1.0 (14 Oct): mini statement JSON on
  Home and the Statement screen; dated PDF export with the password rule.
- Onboarding slides and splash screen, Inter font, shared Icon and
  TypewriterText components, nearby branches via OpenStreetMap.
- Every feature switched on; the 14 Oct switch set is kept in
  featureConfig.stage-2026-10-14.ts. Loans run on demo data until the
  backend has /loans routes.
- npm run android wraps run-android for shells spawned from VS Code.
- OpenProject work packages updated to 14-10-2026 status, with screenshots
  of Positive Pay, statement, loans and cards.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 18:18:40 +05:30

188 lines
6.7 KiB
JavaScript

/**
* Fetches the app's font files into assets/fonts/.
*
* ── WHY THIS SCRIPT IS THE WAY IT IS ────────────────────────────────────────
* An earlier version pointed at a Nunito URL that had died and piped whatever
* came back straight into the .ttf without looking at the HTTP status. So it
* wrote GitHub's 404 page into every font file — 300KB HTML documents with a
* .ttf extension.
*
* Nothing complained. React Native does not validate a font asset: Android
* silently falls back to Roboto, so the app rendered in the system font while
* appearing to have a typeface of its own. That is the failure this script now
* refuses to repeat:
*
* - a non-2xx response aborts instead of being written to disk;
* - every extracted file is checked for the TrueType magic number
* (0x00010000) before it is kept;
* - a file that fails either check is deleted, and the script exits non-zero.
*
* ── WHERE INTER COMES FROM ──────────────────────────────────────────────────
* Inter's static TTFs are not committed to its repository; they ship only in
* the release zip. So this downloads the release once and extracts the five
* weights from `extras/ttf/`. The zip is read with Node's own zlib — there is
* no unzip dependency to install.
*
* ── WHY THE FILENAMES MATTER ────────────────────────────────────────────────
* iOS resolves `fontFamily` against the PostScript name while Android resolves
* it against the asset filename, so the two only agree if the file is named
* after the PostScript name. Inter's release files already are —
* `Inter-SemiBold.ttf` has the PostScript name `Inter-SemiBold` — so they are
* kept exactly as shipped. Do not rename them.
*
* After running this, re-link and rebuild — the files have to be inside the
* binary, so a Metro reload alone will not pick them up:
*
* node download-fonts.js
* npx react-native-asset
* npx react-native run-android
*/
/* eslint-disable no-undef -- A Node build script, not app code: the project's
ESLint environment is React Native and does not declare Node globals. */
const fs = require('fs');
const https = require('https');
const path = require('path');
const zlib = require('zlib');
const INTER_ZIP = 'https://github.com/rsms/inter/releases/download/v4.1/Inter-4.1.zip';
const fonts = ['Inter-Regular.ttf', 'Inter-Medium.ttf', 'Inter-SemiBold.ttf', 'Inter-Bold.ttf', 'Inter-ExtraBold.ttf'];
const dir = path.join(__dirname, 'assets', 'fonts');
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
/** Buffers the body so nothing reaches disk until the status has been checked. */
const fetch = (url, redirects = 0) =>
new Promise((resolve, reject) => {
if (redirects > 5) {
reject(new Error('too many redirects'));
return;
}
https
.get(url, (response) => {
const { statusCode, headers } = response;
if (statusCode === 301 || statusCode === 302 || statusCode === 307 || statusCode === 308) {
response.resume();
fetch(headers.location, redirects + 1).then(resolve, reject);
return;
}
if (statusCode !== 200) {
response.resume();
reject(new Error(`HTTP ${statusCode}`));
return;
}
const chunks = [];
response.on('data', (chunk) => chunks.push(chunk));
response.on('end', () => resolve(Buffer.concat(chunks)));
response.on('error', reject);
})
.on('error', reject);
});
/**
* Pulls one file out of a zip by its basename, via the central directory.
* Handles the two methods a release zip uses: stored (0) and deflate (8).
*/
const extract = (zip, basename) => {
const eocd = zip.lastIndexOf(Buffer.from([0x50, 0x4b, 0x05, 0x06]));
if (eocd < 0) {
throw new Error('not a zip archive');
}
const entries = zip.readUInt16LE(eocd + 10);
let cursor = zip.readUInt32LE(eocd + 16);
for (let i = 0; i < entries; i += 1) {
const method = zip.readUInt16LE(cursor + 10);
const compressedSize = zip.readUInt32LE(cursor + 20);
const nameLength = zip.readUInt16LE(cursor + 28);
const extraLength = zip.readUInt16LE(cursor + 30);
const commentLength = zip.readUInt16LE(cursor + 32);
const localHeader = zip.readUInt32LE(cursor + 42);
const name = zip.toString('utf8', cursor + 46, cursor + 46 + nameLength);
if (name.endsWith(`extras/ttf/${basename}`)) {
const dataStart =
localHeader + 30 + zip.readUInt16LE(localHeader + 26) + zip.readUInt16LE(localHeader + 28);
const data = zip.subarray(dataStart, dataStart + compressedSize);
if (method === 0) {
return data;
}
if (method === 8) {
return zlib.inflateRawSync(data);
}
throw new Error(`unsupported zip method ${method}`);
}
cursor += 46 + nameLength + extraLength + commentLength;
}
throw new Error('not found in the release zip');
};
/**
* A TrueType file opens with sfntVersion 0x00010000. `OTTO` would be an
* OpenType/CFF font, which React Native on Android will not load, and anything
* else — an HTML error page, most likely — is not a font at all.
*/
const isTrueType = (buffer) => buffer.length > 4 && buffer.readUInt32BE(0) === 0x00010000;
const run = async () => {
let zip;
try {
zip = await fetch(INTER_ZIP);
} catch (error) {
console.error(` FAILED ${INTER_ZIP} — ${error.message}`);
process.exitCode = 1;
return;
}
let failed = 0;
for (const font of fonts) {
const dest = path.join(dir, font);
try {
const body = extract(zip, font);
if (!isTrueType(body)) {
const magic = body.length > 4 ? body.readUInt32BE(0).toString(16) : 'empty';
throw new Error(`not a TrueType file (magic 0x${magic}, ${body.length} bytes)`);
}
fs.writeFileSync(dest, body);
console.log(` ok ${font} (${body.length} bytes)`);
} catch (error) {
failed += 1;
// Leave nothing half-written behind: a stale or truncated file here is
// exactly what caused the silent fallback this script exists to prevent.
fs.rmSync(dest, { force: true });
console.error(` FAILED ${font} — ${error.message}`);
}
}
if (failed > 0) {
console.error(`\n${failed} font(s) could not be extracted. Do not build with fonts missing.`);
process.exitCode = 1;
return;
}
console.log('\nAll fonts downloaded and verified.');
console.log('Next: npx react-native-asset && npx react-native run-android');
};
void run();