Loading...
Loading...
Anders & A-Cube S.r.l. / July 15, 2025 • 4 min read
Pempel Dashboard is a production-ready mobile application for managing electronic receipts, built on Expo 53 and React Native 0.79. It provides a seamless offline-first experience with secure mTLS authentication, comprehensive internationalization, and enterprise-grade code quality standards.
┌──────────────────────────────────────────────────┐
│ Expo Router (File-based) │
│ app/(tabs)/* • app/modal/* │
└────────────────┬─────────────────────────────────┘
│
┌────────────────▼─────────────────────────────────┐
│ State Layer (Zustand) │
│ Receipts • Auth • UI • Sync • Settings │
└──────┬──────────────────────────────┬────────────┘
│ │
┌──────▼─────────┐ ┌─────────▼────────────┐
│ Data Layer │ │ Security Layer │
│ - E-Receipt SDK│ │ - expo-mutual-tls │
│ - SQLite │ │ - Secure Store │
│ - AsyncStorage │ │ - Keychain/Keystore │
└────────────────┘ └──────────────────────┘
Expo Ecosystem:
React Native Stack:
State & Data Management:
Receipt List with virtualization:
import { FlashList } from '@shopify/flash-list'
import { useReceiptsStore } from '@/store/receipts'
export default function ReceiptsScreen() {
const { receipts, loading, fetchReceipts } = useReceiptsStore()
return (
<FlashList
data={receipts}
estimatedItemSize={120}
renderItem={({ item }) => <ReceiptCard receipt={item} />}
onRefresh={fetchReceipts}
refreshing={loading}
Receipt Details with PDF viewing:
import { useLocalSearchParams } from 'expo-router'
import { useReceipt } from '@/hooks/useReceipt'
import * as FileSystem from 'expo-file-system'
import * as Sharing from 'expo-sharing'
export default function ReceiptDetails() {
const { id } = useLocalSearchParams()
const { receipt, downloadPDF } = useReceipt(id as string)
const handleShare = async () => {
const pdfUri = await downloadPDF()
await Sharing.
Certificate Setup:
import ExpoMutualTls from '@a-cube-io/expo-mutual-tls'
import * as SecureStore from 'expo-secure-store'
export const setupMTLS = async (credentials: Credentials) => {
try {
// Configure mTLS module
await ExpoMutualTls.configureP12('pempel-app', true)
// Store certificate securely
const certificateData = await fetchUserCertificate(credentials)
await ExpoMutualTls.storeP12(
certificateData.p12Base64,
certificateData.password
)
// Store metadata in secure storage
Authenticated API Calls:
import { EReceiptsSDK } from '@a-cube-io/ereceipts-js-sdk'
// SDK automatically uses mTLS for all requests
export const receiptsSDK = new EReceiptsSDK({
apiUrl: process.env.EXPO_PUBLIC_API_URL,
useMTLS: true,
timeout: 30000
})
// All API calls are mTLS authenticated
export const fetchUserReceipts = async (filters) => {
return await receiptsSDK.getReceipts(filters)
}Sync Strategy:
import { useSyncStore } from '@/store/sync'
import NetInfo from '@react-native-community/netinfo'
export const useSyncManager = () => {
const { lastSync, isSyncing, setLastSync } = useSyncStore()
const syncReceipts = async () => {
const netInfo = await NetInfo.fetch()
if (!netInfo.isConnected) {
return { success: false, reason: 'offline' }
}
setIsSyncing(true)
Translation Setup:
import i18n from 'i18next'
import { initReactI18next } from 'react-i18next'
import * as Localization from 'expo-localization'
i18n
.use(initReactI18next)
.init({
compatibilityJSON: 'v3',
resources: {
en: { translation: require('./locales/en.json') },
it: { translation: require('./locales/it.json') },
fr: { translation: require('./locales/fr.json') }
},
lng: Localization.locale.split(
Usage in Components:
import { useTranslation } from 'react-i18next'
export default function ReceiptsList() {
const { t } = useTranslation()
return (
<View>
<Text>{t('receipts.title')}</Text>
<Text>{t('receipts.emptyState')}</Text>
<Button title={t('receipts.refresh')} />
</View>
)
}Comprehensive Quality Tooling (package.json):
{
"scripts": {
"lint": "eslint . --ext .ts,.tsx,.js,.jsx",
"lint:fix": "eslint . --ext .ts,.tsx,.js,.jsx --fix",
"lint:check": "eslint . --ext .ts,.tsx,.js,.jsx --max-warnings 0",
"lint:unused": "eslint . --ext .ts,.tsx --fix --rule 'unused-imports/no-unused-imports: error'",
"lint:perf": "eslint . --ext .ts,.tsx --rule 'react-perf/jsx-no-new-object-as-prop: error'",
"format": "prettier --write .",
"format:check": "prettier --check .",
"imports:sort": "eslint . --ext .ts,.tsx --fix --rule 'simple-import-sort/imports: error'",
"imports:organize": "npm run imports:sort && npm run lint:unused",
"type:check": "npx tsc --noEmit",
"quality:check": "npm run lint:check && npm run format:check && npm run type:check",
"quality:fix": "npm run lint:fix && npm run format:fix && npm run imports:organize",
Comprehensive Linting Rules:
@typescript-eslint: TypeScript-specific rulesreact-hooks: React hooks best practicesreact-native: React Native specific rulesreact-native-a11y: Accessibility validationreact-perf: Performance optimization checksunused-imports: Detect unused imports/variablessimple-import-sort: Consistent import orderingPre-commit Quality Gates:
{
"lint-staged": {
"*.{ts,tsx,js,jsx}": [
"eslint --fix",
"prettier --write",
"tsc-files --noEmit"
]
}
}Hooks:
Replaced FlatList with FlashList for 10x faster rendering:
import { FlashList } from '@shopify/flash-list'
// Before (FlatList): ~16ms per frame for 1000 items
// After (FlashList): ~2ms per frame for 1000 items
<FlashList
data={receipts}
estimatedItemSize={120} // Critical for performance
renderItem={({ item }) => <MemoizedReceiptCard receipt={item} />}
keyExtractor={item => item.id}
/>Large datasets handled efficiently:
import { useVirtualizer } from '@tanstack/react-virtual'
export function ReceiptsTable({ receipts }) {
const parentRef = useRef(null)
const virtualizer = useVirtualizer({
count: receipts.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 50,
overscan: 5
})
return (
<ScrollView ref={parentRef}>
{virtualizer.getVirtualItems().map
Memoization Strategy:
import { memo, useMemo, useCallback } from 'react'
const ReceiptCard = memo(({ receipt, onPress }) => {
const formattedDate = useMemo(
() => formatDate(receipt.date),
[receipt.date]
)
const handlePress = useCallback(() => {
onPress(receipt.id)
}, [receipt.id, onPress])
return (
<Pressable onPress=
# 1. Install dependencies
bun install
# 2. Start Expo dev server
bun start
# 3. Run on iOS
bun ios
# 4. Run on Android
bun android
# 5. Run quality checks
bun run quality:check.env.local:
EXPO_PUBLIC_API_URL=https://api-dev.acubeapi.com
EXPO_PUBLIC_ENABLE_LOGS=true
EXPO_PUBLIC_CACHE_TTL=3600000# EAS Build
eas build --platform ios --profile production
eas build --platform android --profile production
# Submit to app stores
eas submit --platform ios
eas submit --platform android✅ What Worked Well:
📚 Key Takeaways:
Pempel Dashboard demonstrates production-ready mobile development with Expo and React Native, achieving enterprise-grade quality through comprehensive tooling and best practices. The project successfully balances performance, security, and developer experience.
License: Proprietary (A-Cube S.r.l.)