Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 15 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@ Android-only React Native app, distributed via GitHub Releases for installation
[Obtainium](https://github.com/ImranR98/Obtainium). Targets GrapheneOS and stock Android;
intentionally avoids Google Play Services, Firebase, and Play Integrity.

Connects to an Elixir/Phoenix GraphQL API. Types and hooks are code-generated from the
schema with [`graphql-codegen`](https://the-guild.dev/graphql/codegen) + Apollo Client.
Connects to an Elixir/Phoenix GraphQL API. Operation types and typed documents are
code-generated from the schema with [`graphql-codegen`](https://the-guild.dev/graphql/codegen)
and used with Apollo Client's own hooks.

## Stack

- React Native 0.85 (bare, TypeScript, no Expo)
- Apollo Client 3 + `@apollo/client`
- `@graphql-codegen/cli` with `typescript`, `typescript-operations`, `typescript-react-apollo`
- Apollo Client 4 (`@apollo/client`)
- `@graphql-codegen/cli` with `typescript-operations` + `typed-document-node`
- Package manager: **bun** (lockfile: `bun.lock`). Node is still required at runtime — Metro and Gradle's react.gradle plugin invoke `node` directly.
- Linter + formatter: **biome** (`biome.json`) — single tool, replaces eslint + prettier
- Toolchain pinned via [mise](https://mise.jdx.dev) (`mise.toml`): Node 22, Bun 1.2, JDK 17 (Zulu)
Expand Down Expand Up @@ -159,18 +160,25 @@ Output: `src/graphql/__generated__/types.ts` — committed to the repo. CI
doesn't run codegen (no schema in CI), so remember to regenerate and commit
after editing a `.graphql` file.

Example of using a generated hook:
Codegen emits a `TypedDocumentNode` per operation (plus its result/variable
types) — not hooks. Pass the document to Apollo's own hook and both the result
and the variables are inferred from it:

```tsx
import { usePingQuery } from './src/graphql/__generated__/types';
import { useQuery } from '@apollo/client/react';
import { PingDocument } from './src/graphql/__generated__/types';

function Ping() {
const { data, loading } = usePingQuery();
const { data, loading } = useQuery(PingDocument);
if (loading) return <Text>…</Text>;
return <Text>{data?.ping}</Text>;
}
```

`useMutation(SomeDocument)` and `useLazyQuery(SomeDocument)` work the same way.
Deliberately no `typescript-react-apollo`: its generated hooks are Apollo v3-shaped
and it pins `graphql` to <=16.

## Releasing (signed APK to GitHub Releases)

The `Release APK` workflow (`.github/workflows/release.yml`) builds a signed APK on
Expand Down
105 changes: 105 additions & 0 deletions __tests__/metadataRows.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/**
* @format
*/

import {metadataRows} from '../src/map/metadataRows';

const titles = (rows: ReturnType<typeof metadataRows>) =>
rows.map(row => `${row.title}: ${row.value}`);

test('an element with no metadata has no rows', () => {
expect(metadataRows(null)).toEqual([]);
expect(metadataRows(undefined)).toEqual([]);
expect(metadataRows({})).toEqual([]);
});

test('rows read in booking order, not the order the fields arrived', () => {
expect(
titles(
metadataRows({
seat: '14C',
arrivalLocation: 'Barcelona',
number: 'IB3216',
departureLocation: 'Madrid',
reservation: 'XY7ZQ2',
}),
),
).toEqual([
'Number: IB3216',
'From: Madrid',
'To: Barcelona',
'Reservation: XY7ZQ2',
'Seat: 14C',
]);
});

test('the kind of transportation titles the number', () => {
expect(titles(metadataRows({type: 'flight', number: 'IB3216'}))).toEqual([
'Flight: IB3216',
]);
expect(titles(metadataRows({type: 'train', number: 'AVE 3092'}))).toEqual([
'Train: AVE 3092',
]);
expect(titles(metadataRows({type: 'ferry', number: '7'}))).toEqual([
'Ferry: 7',
]);
});

test('a kind with no name of its own keeps the generic title', () => {
expect(titles(metadataRows({type: 'car', number: 'ABC123'}))).toEqual([
'Number: ABC123',
]);
expect(titles(metadataRows({number: 'ABC123'}))).toEqual(['Number: ABC123']);
});

test('type alone is never a row of its own', () => {
expect(metadataRows({type: 'flight'})).toEqual([]);
});

test('a flight number links to its status, other numbers do not', () => {
const [flight] = metadataRows({type: 'flight', number: 'American 291'});
expect(flight.link).toBe(
'https://www.google.com/search?q=American%20291%20flight%20status',
);

const [train] = metadataRows({type: 'train', number: 'AVE 3092'});
expect(train.link).toBeNull();
});

test('the raw address only shows when there is no geocoded location', () => {
const metadata = {address: '12 Gran Via, Madrid'};
expect(titles(metadataRows(metadata, {hasLocation: false}))).toEqual([
'Address: 12 Gran Via, Madrid',
]);
expect(metadataRows(metadata, {hasLocation: true})).toEqual([]);
});

test('a location does not suppress the other rows', () => {
expect(
titles(
metadataRows(
{reservation: 'XY7ZQ2', address: '12 Gran Via, Madrid'},
{hasLocation: true},
),
),
).toEqual(['Reservation: XY7ZQ2']);
});

test('blank and whitespace-only values are skipped, and values are trimmed', () => {
expect(
titles(
metadataRows({
number: ' IB3216 ',
seat: '',
reservation: ' ',
paymentDetails: null,
}),
),
).toEqual(['Number: IB3216']);
});

test('payment notes are titled Payment', () => {
expect(titles(metadataRows({paymentDetails: 'Paid €100'}))).toEqual([
'Payment: Paid €100',
]);
});
Loading
Loading