Skip to content
Open
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
7 changes: 3 additions & 4 deletions .github/workflows/mobile.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
name: Mobile CI

on:
workflow_dispatch:
push:
Expand All @@ -21,10 +20,10 @@ jobs:

steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v5

- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v5
with:
node-version: '20.x'
cache: 'npm'
Expand All @@ -34,7 +33,7 @@ jobs:
run: npm ci

- name: Type check
run: npm run tsc -- --noEmit
run: npm run tsc -- noEmit

- name: Run unit tests with coverage
run: npm run test:coverage
141 changes: 141 additions & 0 deletions apps/mobile/app/settings/status.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
ActivityIndicator,
SafeAreaView,
ScrollView,
Share,
StyleSheet,
Text,
TouchableOpacity,
Expand All @@ -14,6 +15,7 @@ import { useEnvironment } from '../../contexts/EnvironmentContext';
import { useLocalization } from '../../src/context';
import { config } from '../../lib/config';
import { getReleaseMetadata, ReleaseInfo } from '../../lib/release-metadata';
import * as Updates from 'expo-updates';
import axios from 'axios';

type ConnectionStatus = 'idle' | 'testing' | 'online' | 'offline';
Expand All @@ -25,11 +27,21 @@ export default function StatusScreen() {
const [releaseNotes, setReleaseNotes] = useState<ReleaseInfo[]>([]);
const [connectionStatus, setConnectionStatus] = useState<ConnectionStatus>('idle');
const [lastChecked, setLastChecked] = useState<string | null>(null);
const [runtimeVersion, setRuntimeVersion] = useState<string>('N/A');
const [updateId, setUpdateId] = useState<string>('N/A');
const [channel, setChannel] = useState<string>('N/A');
const [updateAvailable, setUpdateAvailable] = useState(false);
const [updateChecked, setUpdateChecked] = useState(false);
const [checkingForUpdate, setCheckingForUpdate] = useState(false);

useEffect(() => {
// Load release notes using utility helper
const data = getReleaseMetadata();
setReleaseNotes(data.releases);
// Surface current update metadata from expo-updates (falls back to N/A in dev)
setRuntimeVersion(Updates.runtimeVersion ?? 'N/A');
setUpdateId(Updates.updateId ?? 'N/A');
setChannel(Updates.channel ?? 'N/A');
}, []);

const handleTestConnection = async () => {
Expand Down Expand Up @@ -66,6 +78,38 @@ export default function StatusScreen() {
}
};

const handleCheckForUpdate = async () => {
if (checkingForUpdate) return;
setCheckingForUpdate(true);
setUpdateChecked(false);
try {
const result = await Updates.checkForUpdateAsync();
setUpdateAvailable(result.isAvailable);
} catch (error) {
console.warn('Update check failed:', error);
setUpdateAvailable(false);
} finally {
setUpdateChecked(true);
setCheckingForUpdate(false);
}
};

const handleCopyDiagnostics = async () => {
const diagnostics = [
`${config.app.name} MVP β€” ${config.app.version} (${config.app.variant})`,
`Environment: ${environmentConfig.label} (${environment})`,
`API URL: ${environmentConfig.apiBaseUrl || 'Not configured'}`,
`Stellar Network: ${environmentConfig.stellarNetwork}`,
`Soroban RPC: ${environmentConfig.sorobanRpcUrl || 'Not configured'}`,
`Runtime Version: ${runtimeVersion}`,
`Update ID: ${updateId}`,
`Channel: ${channel}`,
`Update checked: ${updateChecked ? (updateAvailable ? 'available' : 'up-to-date') : 'never'}`,
`Last connection test: ${lastChecked || 'never'}`,
].join('\n');
await Share.share({ message: diagnostics });
};

const getStatusColor = (status: ConnectionStatus) => {
switch (status) {
case 'online':
Expand Down Expand Up @@ -281,6 +325,103 @@ export default function StatusScreen() {
)}
</View>

{/* Update Information Section */}
<View style={[styles.card, { backgroundColor: colors.surface, borderColor: colors.border }]}>
<View style={styles.cardHeader}>
<Ionicons name="refresh-circle-outline" size={20} color={colors.accent} />
<Text style={[styles.cardTitle, { color: colors.text }]}>Update Information</Text>
</View>

<View style={styles.detailRow}>
<Text style={[styles.detailLabel, { color: colors.textSecondary }]}>Runtime Version</Text>
<Text style={[styles.detailValue, { color: colors.text }]}>{runtimeVersion}</Text>
</View>

<View style={[styles.detailDivider, { backgroundColor: colors.border }]} />

<View style={styles.detailRow}>
<Text style={[styles.detailLabel, { color: colors.textSecondary }]}>Update ID</Text>
<Text style={[styles.detailValue, { color: colors.text }]} numberOfLines={1} ellipsizeMode="middle">
{updateId}
</Text>
</View>

<View style={[styles.detailDivider, { backgroundColor: colors.border }]} />

<View style={styles.detailRow}>
<Text style={[styles.detailLabel, { color: colors.textSecondary }]}>Channel</Text>
<Text style={[styles.detailValue, { color: colors.text }]}>{channel}</Text>
</View>

<View style={[styles.detailDivider, { backgroundColor: colors.border }]} />

<View style={styles.detailRow}>
<Text style={[styles.detailLabel, { color: colors.textSecondary }]}>Update</Text>
<Text
style={[
styles.detailValue,
{
color: updateChecked
? updateAvailable
? colors.success
: colors.textSecondary
: colors.textSecondary,
},
]}
>
{!updateChecked
? 'Not checked'
: updateAvailable
? 'Update available'
: 'Up to date'}
</Text>
</View>

<TouchableOpacity
style={[
styles.testButton,
{
backgroundColor: colors.card,
borderColor: colors.border,
marginTop: 16,
},
]}
onPress={handleCheckForUpdate}
activeOpacity={0.7}
accessibilityRole="button"
accessibilityLabel="Check for updates"
>
{checkingForUpdate ? (
<ActivityIndicator size="small" color={colors.warning} />
) : (
<Text style={[styles.testButtonText, { color: colors.text }]}>Check for Updates</Text>
)}
</TouchableOpacity>
</View>

{/* Diagnostics Section */}
<View style={[styles.card, { backgroundColor: colors.surface, borderColor: colors.border }]}>
<View style={styles.cardHeader}>
<Ionicons name="copy-outline" size={20} color={colors.accent} />
<Text style={[styles.cardTitle, { color: colors.text }]}>Copy Diagnostics</Text>
</View>
<TouchableOpacity
style={[
styles.testButton,
{
backgroundColor: colors.card,
borderColor: colors.border,
},
]}
onPress={handleCopyDiagnostics}
activeOpacity={0.7}
accessibilityRole="button"
accessibilityLabel="Copy diagnostics"
>
<Text style={[styles.testButtonText, { color: colors.text }]}>Copy Diagnostics</Text>
</TouchableOpacity>
</View>

{/* App Meta Section */}
<View style={styles.metaContainer}>
<Text style={[styles.metaText, { color: colors.textSecondary }]}>
Expand Down
26 changes: 26 additions & 0 deletions apps/mobile/lib/release-metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ export interface ReleaseMetadata {
releases: ReleaseInfo[];
}

export interface UpdateInfo {
runtimeVersion: string | null;
updateId: string | null;
channel: string | null;
isEmbedded: boolean;
}

export const fallbackReleaseMetadata: ReleaseMetadata = {
releases: [
{
Expand Down Expand Up @@ -42,3 +49,22 @@ export function getReleaseMetadata(): ReleaseMetadata {
}
return fallbackReleaseMetadata;
}

/**
* Retrieves the current OTA update information from expo-updates.
* Returns null values when no update metadata is available (e.g. in a development client).
*/
export function getUpdateInfo(): UpdateInfo {
try {
const Updates = require('expo-updates');
return {
runtimeVersion: Updates.runtimeVersion ?? null,
updateId: Updates.updateId ?? null,
channel: Updates.channel ?? null,
isEmbedded: Updates.isEmbeddedLaunch ?? false,
};
} catch (error) {
console.warn('Unable to load expo-updates, using fallback update info:', error);
return { runtimeVersion: null, updateId: null, channel: null, isEmbedded: false };
}
}
Loading