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
26 changes: 24 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ jobs:
strategy:
fail-fast: false
matrix:
app: [tictactogether, stocksync, orderup, cowrite, outagenet, yardgate, eventfloor, agrimesh, chargecatch]
app: [tictactogether, stocksync, orderup, cowrite, outagenet, yardgate, eventfloor, agrimesh, chargecatch, surfrroll]
steps:
- uses: actions/checkout@v7
- uses: pnpm/action-setup@v6
Expand All @@ -62,10 +62,30 @@ jobs:
distribution: temurin
java-version: 17
cache: gradle
# One cache key per app — avoids matrix jobs overwriting the same Gradle
# cache while Reanimated/Worklets prefab packages are half-built.
cache-dependency-path: |
apps/${{ matrix.app }}/android/**/*.gradle*
apps/${{ matrix.app }}/android/gradle/wrapper/gradle-wrapper.properties
- run: pnpm install --frozen-lockfile
- name: Build debug APK
working-directory: apps/${{ matrix.app }}/android
run: ./gradlew assembleDebug -PreactNativeArchitectures=x86_64 --no-daemon
run: |
GRADLE=(./gradlew -PreactNativeArchitectures=x86_64 --no-daemon)
# Prefab packages must exist before :app:configureCMakeDebug (Reanimated race).
PREFAB=(
:react-native-worklets:prefabDebugPackage
:react-native-reanimated:prefabDebugPackage
)
for attempt in 1 2 3; do
if "${GRADLE[@]}" "${PREFAB[@]}" assembleDebug; then
exit 0
fi
echo "::warning::assembleDebug failed (attempt $attempt), clearing app native build dirs"
rm -rf app/build app/.cxx
sleep $((attempt * 10))
done
exit 1

ios:
needs: check
Expand Down Expand Up @@ -93,6 +113,8 @@ jobs:
scheme: AgriMesh
- app: chargecatch
scheme: ChargeCatch
- app: surfrroll
scheme: SurfrRoll
steps:
- uses: actions/checkout@v7
- uses: pnpm/action-setup@v6
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
node_modules/
.pnpm-store/
.turbo/
.DS_Store
*.log
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ accounts or server, and shows a different way to keep shared state in sync.
| Event Floor | [`apps/eventfloor`](./apps/eventfloor) | Live events: Flow A staff MLS dispatch + Flow B gate admission ledger and mock platform sync (`scanId` dedupe). |
| AgriMesh | [`apps/agrimesh`](./apps/agrimesh) | Agriculture: field readings store-and-forward over multi-hop Bluetooth mesh, office-edge ledger, mock farm sync (`readingId` dedupe). |
| ChargeCatch | [`apps/chargecatch`](./apps/chargecatch) | EV charge site: local session ledger on charge points, site hub over Bluetooth mesh, mock CMS reconciliation when head office link returns (`sessionId` dedupe). |
| SurfrRoll | [`apps/surfrroll`](./apps/surfrroll) | MitKat DataSurfr last mile: signed incident alerts and staff roll call (safe / help / SOS) over Bluetooth mesh, mock SOC upload when cloud link returns (`checkInId` dedupe). |

Each app's README explains how to run it, how its sync works and what its tests cover.

Expand Down
4 changes: 4 additions & 0 deletions apps/surfrroll/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module.exports = {
root: true,
extends: '@react-native',
};
16 changes: 16 additions & 0 deletions apps/surfrroll/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
node_modules/
.expo/
dist/
build/
ios/Pods/
ios/build/
android/app/build/
android/build/
android/.gradle/
**/.cxx/
android/local.properties
*.jsbundle
.DS_Store
.env*
!.env.example
ios/.xcode.env.local
1 change: 1 addition & 0 deletions apps/surfrroll/.prettierrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"singleQuote":true,"trailingComma":"all","bracketSpacing":true}
194 changes: 194 additions & 0 deletions apps/surfrroll/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
import '@offline-app-examples/ui/global.css';
import { Button, Text } from '@offline-app-examples/ui';
import React, { useState } from 'react';
import { Pressable, ScrollView, StatusBar, TextInput, View } from 'react-native';
import { SafeAreaProvider, SafeAreaView } from 'react-native-safe-area-context';
import { DEMO_CLIENT, EMPLOYEE_ROSTER, signInEmployee, type EmployeeSession } from './src/domain/roster';
import { SurfrFlow, type SurfrRole } from './src/flows/SurfrFlow';
import { SurfrHero } from './src/ui/illustrations';

type Setup = {
role: SurfrRole;
consoleName: string;
employee: EmployeeSession | null;
};

export default function App() {
const [setup, setSetup] = useState<Setup | null>(null);

if (setup) {
return (
<SafeAreaProvider>
<StatusBar barStyle="dark-content" />
<SurfrFlow
role={setup.role}
consoleName={setup.consoleName}
employee={setup.employee}
onLeave={() => setSetup(null)}
/>
</SafeAreaProvider>
);
}

return (
<SafeAreaProvider>
<StatusBar barStyle="dark-content" />
<HomeLobby onStart={setSetup} />
</SafeAreaProvider>
);
}

function HomeLobby({ onStart }: { onStart: (setup: Setup) => void }) {
const [role, setRole] = useState<SurfrRole>('soc');
const [name, setName] = useState('Mumbai SOC');
const [employeeId, setEmployeeId] = useState(EMPLOYEE_ROSTER[0].employeeId);
const [pin, setPin] = useState('');

const defaults: Record<SurfrRole, string> = {
soc: 'Mumbai SOC',
staff: EMPLOYEE_ROSTER[0].displayName,
relay: 'Campus relay',
};

return (
<SafeAreaView className="bg-background flex-1" edges={['top', 'left', 'right', 'bottom']}>
<ScrollView
className="flex-1"
contentContainerClassName="px-6 pt-4 pb-10"
keyboardShouldPersistTaps="handled"
showsVerticalScrollIndicator
>
<Text className="font-serif text-4xl">SurfrRoll</Text>
<Text className="text-muted-foreground mt-2 text-base leading-6">
{DEMO_CLIENT.name} — last mile for MitKat DataSurfr. Signed incident alerts and employee roll
call hop over Bluetooth mesh when cell and internet fail; check-ins reconcile to a mock SOC
when the cloud link returns (SDK 0.28).
</Text>
<View className="my-6 items-center">
<SurfrHero size={160} />
</View>
<RolePick
options={[
{
id: 'soc',
title: 'SOC console',
hint: 'Receive DataSurfr alerts, fan out signed incident, upload roll call',
},
{
id: 'staff',
title: 'Staff handset',
hint: 'Verify alert, tap safe / help / SOS over mesh',
},
{
id: 'relay',
title: 'Mesh relay',
hint: 'Extend range when only one phone has internet',
},
]}
selected={role}
onSelect={(id) => {
const r = id as SurfrRole;
setRole(r);
setName(defaults[r]);
if (r === 'staff') {
setEmployeeId(EMPLOYEE_ROSTER[0].employeeId);
setPin('');
}
}}
/>
{role === 'staff' ? (
<>
<Text className="text-muted-foreground mb-2 text-xs font-bold uppercase tracking-widest">
Employee (demo roster)
</Text>
<View className="mb-3 gap-2">
{EMPLOYEE_ROSTER.map((e) => (
<Pressable
key={e.employeeId}
onPress={() => {
setEmployeeId(e.employeeId);
setName(e.displayName);
setPin('');
}}
className={`rounded-xl border-2 px-4 py-3 ${employeeId === e.employeeId ? 'border-foreground bg-muted' : 'border-muted'}`}
>
<Text className="font-serif text-lg">{e.displayName}</Text>
<Text className="text-muted-foreground text-sm">
{e.office} · PIN on card
</Text>
</Pressable>
))}
</View>
<Text className="text-muted-foreground mb-2 text-xs font-bold uppercase tracking-widest">
PIN
</Text>
<TextInput
className="border-foreground text-foreground mb-4 rounded-lg border-2 px-3 py-3"
value={pin}
onChangeText={setPin}
placeholder="Demo PIN"
keyboardType="number-pad"
secureTextEntry
/>
</>
) : null}
<Text className="text-muted-foreground mb-2 text-xs font-bold uppercase tracking-widest">
Device label
</Text>
<TextInput
className="border-foreground text-foreground mb-4 rounded-lg border-2 px-3 py-3"
value={name}
onChangeText={setName}
placeholder={defaults[role]}
/>
<Button
disabled={role === 'staff' && !signInEmployee(employeeId, pin)}
onPress={() => {
if (role === 'staff') {
const session = signInEmployee(employeeId, pin);
if (!session) return;
onStart({
role,
consoleName: session.displayName,
employee: session,
});
return;
}
onStart({
role,
consoleName: name.trim() || defaults[role],
employee: null,
});
}}
>
<Text>Start</Text>
</Button>
</ScrollView>
</SafeAreaView>
);
}

function RolePick({
options,
selected,
onSelect,
}: {
options: { id: string; title: string; hint: string }[];
selected: string;
onSelect: (id: string) => void;
}) {
return (
<View className="my-4 gap-2">
{options.map((o) => (
<Pressable
key={o.id}
onPress={() => onSelect(o.id)}
className={`rounded-xl border-2 px-4 py-3 ${selected === o.id ? 'border-foreground bg-muted' : 'border-muted'}`}
>
<Text className="font-serif text-xl">{o.title}</Text>
<Text className="text-muted-foreground text-sm">{o.hint}</Text>
</Pressable>
))}
</View>
);
}
17 changes: 17 additions & 0 deletions apps/surfrroll/Gemfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
source 'https://rubygems.org'

# You may use http://rbenv.org/ or https://rvm.io/ to install and use this version
ruby ">= 2.6.10"

# Exclude problematic versions of cocoapods and activesupport that causes build failures.
gem 'cocoapods', '~> 1.16.0'
gem 'activesupport', '>= 6.1.7.5', '!= 7.1.0'
gem 'xcodeproj', '>= 1.25.0', '< 1.28.0'
gem 'concurrent-ruby', '< 1.3.4'

# Ruby 3.4.0 has removed some libraries from the standard library.
gem 'bigdecimal'
gem 'logger'
gem 'benchmark'
gem 'mutex_m'
gem 'nkf'
Loading
Loading