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>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
5fbd13ba12
commit
f960a119e8
+79
-29
@@ -2,11 +2,9 @@
|
||||
* Fetches the app's font files into assets/fonts/.
|
||||
*
|
||||
* ── WHY THIS SCRIPT IS THE WAY IT IS ────────────────────────────────────────
|
||||
* The previous version pointed at `googlefonts/nunito/raw/main/fonts/ttf/`,
|
||||
* which no longer exists — that repo now ships variable fonts only. It also
|
||||
* piped whatever came back straight into the .ttf without looking at the HTTP
|
||||
* status. So running it after the URLs died wrote GitHub's 404 page into all
|
||||
* four font files, and every one of them was a 300KB HTML document with a
|
||||
* 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
|
||||
@@ -15,17 +13,22 @@
|
||||
* refuses to repeat:
|
||||
*
|
||||
* - a non-2xx response aborts instead of being written to disk;
|
||||
* - every download is checked for the TrueType magic number (0x00010000)
|
||||
* before it is kept;
|
||||
* - 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.
|
||||
*
|
||||
* ── WHY THE FILENAMES LOOK ODD ──────────────────────────────────────────────
|
||||
* They are the fonts' own PostScript names, not IBM's file names. 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. IBM abbreviates: SemiBold is `SmBld`, Medium is `Medm`, and
|
||||
* Regular has no suffix at all. Renaming them here is what lets
|
||||
* `fontFamily: 'IBMPlexSans-SmBld'` work on both platforms.
|
||||
* ── 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:
|
||||
@@ -41,18 +44,12 @@
|
||||
const fs = require('fs');
|
||||
const https = require('https');
|
||||
const path = require('path');
|
||||
const zlib = require('zlib');
|
||||
|
||||
const PLEX = 'https://raw.githubusercontent.com/IBM/plex/master/packages';
|
||||
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'];
|
||||
|
||||
/** `as` is the PostScript name — see the note above on why that is the filename. */
|
||||
const fonts = [
|
||||
{ as: 'IBMPlexSans.ttf', url: `${PLEX}/plex-sans/fonts/complete/ttf/IBMPlexSans-Regular.ttf` },
|
||||
{ as: 'IBMPlexSans-Medm.ttf', url: `${PLEX}/plex-sans/fonts/complete/ttf/IBMPlexSans-Medium.ttf` },
|
||||
{ as: 'IBMPlexSans-SmBld.ttf', url: `${PLEX}/plex-sans/fonts/complete/ttf/IBMPlexSans-SemiBold.ttf` },
|
||||
{ as: 'IBMPlexSans-Bold.ttf', url: `${PLEX}/plex-sans/fonts/complete/ttf/IBMPlexSans-Bold.ttf` },
|
||||
{ as: 'IBMPlexMono.ttf', url: `${PLEX}/plex-mono/fonts/complete/ttf/IBMPlexMono-Regular.ttf` },
|
||||
{ as: 'IBMPlexMono-SmBld.ttf', url: `${PLEX}/plex-mono/fonts/complete/ttf/IBMPlexMono-SemiBold.ttf` },
|
||||
];
|
||||
|
||||
const dir = path.join(__dirname, 'assets', 'fonts');
|
||||
|
||||
@@ -92,6 +89,49 @@ const fetch = (url, redirects = 0) =>
|
||||
.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
|
||||
@@ -100,13 +140,23 @@ const fetch = (url, redirects = 0) =>
|
||||
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.as);
|
||||
const dest = path.join(dir, font);
|
||||
|
||||
try {
|
||||
const body = await fetch(font.url);
|
||||
const body = extract(zip, font);
|
||||
|
||||
if (!isTrueType(body)) {
|
||||
const magic = body.length > 4 ? body.readUInt32BE(0).toString(16) : 'empty';
|
||||
@@ -114,18 +164,18 @@ const run = async () => {
|
||||
}
|
||||
|
||||
fs.writeFileSync(dest, body);
|
||||
console.log(` ok ${font.as} (${body.length} bytes)`);
|
||||
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.as} — ${error.message}`);
|
||||
console.error(` FAILED ${font} — ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (failed > 0) {
|
||||
console.error(`\n${failed} font(s) could not be fetched. Do not build with fonts missing.`);
|
||||
console.error(`\n${failed} font(s) could not be extracted. Do not build with fonts missing.`);
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user