@sincpro/mobile 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -12
- package/dist/adapters/repositories/domain_event.repository.js +2 -1
- package/dist/adapters/repositories/domain_event_dead_letter.repository.js +2 -1
- package/dist/adapters/repositories/setting.repository.js +4 -3
- package/dist/domain/settings.d.ts +1 -1
- package/dist/domain/settings.js +4 -1
- package/dist/entrypoints/db/migrations.d.ts +1 -1
- package/dist/entrypoints/db/migrations.js +26 -18
- package/dist/entrypoints/queue/printImage.subscriber.js +3 -3
- package/dist/entrypoints/ui/AppShell.d.ts +7 -0
- package/dist/entrypoints/ui/AppShell.js +25 -9
- package/dist/entrypoints/ui/common_provider.d.ts +6 -1
- package/dist/entrypoints/ui/common_provider.js +45 -1
- package/dist/entrypoints/ui/domain_switcher.js +5 -4
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/infrastructure/logger.d.ts +2 -2
- package/dist/infrastructure/logger.js +51 -33
- package/dist/infrastructure/ui/ToastHost.d.ts +6 -1
- package/dist/infrastructure/ui/ToastHost.js +18 -9
- package/dist/services/printer.service.d.ts +6 -0
- package/dist/services/printer.service.js +40 -6
- package/dist/ui/components/atoms/DebugBanner.js +4 -8
- package/dist/ui/components/atoms/PrinterIcon.d.ts +4 -0
- package/dist/ui/components/atoms/PrinterIcon.js +4 -0
- package/dist/ui/components/atoms/index.d.ts +1 -0
- package/dist/ui/components/atoms/index.js +1 -0
- package/dist/ui/components/molecules/BluetoothDeviceSelectorModal.js +16 -10
- package/dist/ui/components/molecules/DeadLetterErrorBlock.js +3 -2
- package/dist/ui/components/molecules/EventTimelineItem.js +31 -33
- package/dist/ui/components/molecules/GeoPermissionCard.d.ts +8 -0
- package/dist/ui/components/molecules/GeoPermissionCard.js +21 -0
- package/dist/ui/components/molecules/ProcessToast.d.ts +7 -4
- package/dist/ui/components/molecules/ProcessToast.js +45 -44
- package/dist/ui/components/molecules/ProcessToastProvider.js +1 -1
- package/dist/ui/components/molecules/TimeZoneSelector.d.ts +7 -0
- package/dist/ui/components/molecules/TimeZoneSelector.js +19 -0
- package/dist/ui/components/molecules/TimeZoneSelectorModal.d.ts +9 -0
- package/dist/ui/components/molecules/TimeZoneSelectorModal.js +61 -0
- package/dist/ui/components/molecules/index.d.ts +4 -0
- package/dist/ui/components/molecules/index.js +3 -0
- package/dist/ui/components/molecules/timezone.d.ts +4 -0
- package/dist/ui/components/molecules/timezone.js +1 -0
- package/dist/ui/components/organisms/BluetoothPrinterSelector.d.ts +3 -1
- package/dist/ui/components/organisms/BluetoothPrinterSelector.js +29 -9
- package/dist/ui/components/organisms/DatabaseInfoRow.js +1 -1
- package/dist/ui/components/organisms/DeadLetterQueueRow.js +3 -3
- package/dist/ui/components/organisms/EventRow.js +15 -15
- package/dist/ui/components/organisms/InjectableWebView.js +20 -10
- package/dist/ui/components/organisms/TableInfoInfoRow.js +1 -1
- package/dist/ui/screens/database/database.json_detail.js +3 -2
- package/dist/ui/screens/database/database.list.js +4 -4
- package/dist/ui/screens/database/database.table_rows.js +1 -1
- package/dist/ui/screens/dead_letter_queue/dead_letter_queue.list.js +4 -4
- package/dist/ui/screens/events/events.list.js +4 -4
- package/package.json +9 -5
- package/dist/ui/layouts/router_layouts.d.ts +0 -17
- package/dist/ui/layouts/router_layouts.js +0 -20
package/README.md
CHANGED
|
@@ -1,6 +1,27 @@
|
|
|
1
1
|
# @sincpro/mobile
|
|
2
2
|
|
|
3
|
-
Core del framework móvil Sincpro (React Native / Expo): infraestructura offline-first (DB, cola/eventos, cron), patrones DDD, adapters base, composición module-driven y el `AppShell`. Consume el design system `@sincpro/mobile-ui`.
|
|
3
|
+
Core del **framework móvil Sincpro** (React Native / Expo): infraestructura offline-first (DB, cola/eventos, cron), patrones DDD, adapters base, composición **module-driven** y el `AppShell`. Consume el design system `@sincpro/mobile-ui`.
|
|
4
|
+
|
|
5
|
+
> 🤖 **¿Sos un agente de IA?** Leé primero [`AGENTS.md`](AGENTS.md) — orientación rápida del ecosistema, patrones y trampas. Si algo se comporta raro, [`docs/GOTCHAS.md`](docs/GOTCHAS.md).
|
|
6
|
+
|
|
7
|
+
## Filosofía
|
|
8
|
+
|
|
9
|
+
El objetivo es **generar apps móviles de negocio muy rápido** componiendo piezas estables, no escribir cada app desde cero. Principios:
|
|
10
|
+
|
|
11
|
+
- **Framework, no monolito.** Un ecosistema de paquetes publicables; cada app es delgada y declara solo sus dominios.
|
|
12
|
+
- **Module-driven + inversión de dependencia.** El core no conoce los negocios. Cada dominio es un `DomainModule` que la app registra; el `orchestrator` alimenta cola, repos, migraciones, cron y temas desde los módulos registrados. Agregar/mover un dominio no toca el core.
|
|
13
|
+
- **Offline-first.** SQLite + cola de eventos persistente (con DLQ y reintentos) + sincronización; la app funciona sin red y reconcilia al volver.
|
|
14
|
+
- **Hexagonal.** El dominio define puertos (`IPrinterDriver`, `IRepository`, `IRemoteClient`); los adapters concretos viven afuera y se inyectan. El core es agnóstico de hardware y de backend.
|
|
15
|
+
- **Frontera real verificable.** Capas con dirección estricta `apps → mobile-odoo → mobile → mobile-ui`; el design system no sabe de negocio; los enums/eventos tienen un punto único de export.
|
|
16
|
+
|
|
17
|
+
## Ecosistema
|
|
18
|
+
|
|
19
|
+
| Paquete | Rol |
|
|
20
|
+
| ---------------------- | ------------------------------------------------------------------------------------------- |
|
|
21
|
+
| `@sincpro/mobile` | Core: infra, patrones DDD, `framework/` (DomainModule, createApp, orchestrator), `AppShell` |
|
|
22
|
+
| `@sincpro/mobile-ui` | Design system standalone (sin router, sin dominios) |
|
|
23
|
+
| `@sincpro/mobile-odoo` | Integración Odoo **opcional** (OdooClient, auth, server, partner) |
|
|
24
|
+
| `sincpro-mobile-<app>` | Apps de negocio (tickets, distribution) que componen lo anterior |
|
|
4
25
|
|
|
5
26
|
## Instalación
|
|
6
27
|
|
|
@@ -15,8 +36,9 @@ npx expo install @sincpro/mobile @sincpro/mobile-ui
|
|
|
15
36
|
Una app define sus dominios como **clases que extienden `DomainModule`** y las compone con `createAppShell`:
|
|
16
37
|
|
|
17
38
|
```tsx
|
|
18
|
-
import { createAppShell, createTheme
|
|
19
|
-
import
|
|
39
|
+
import { createAppShell, createTheme } from "@sincpro/mobile";
|
|
40
|
+
import { DomainModule } from "@sincpro/mobile/framework/domain_module";
|
|
41
|
+
import type { Subscriber } from "@sincpro/mobile/domain/event_sourcing";
|
|
20
42
|
|
|
21
43
|
class VentasModule extends DomainModule {
|
|
22
44
|
readonly key = "VENTAS";
|
|
@@ -55,20 +77,15 @@ export default createAppShell({
|
|
|
55
77
|
Las primitivas y capas se importan por subpath, p. ej.:
|
|
56
78
|
|
|
57
79
|
```ts
|
|
58
|
-
import { DomainModule } from "@sincpro/mobile/
|
|
59
|
-
import type { Subscriber } from "@sincpro/mobile/domain/
|
|
80
|
+
import { DomainModule } from "@sincpro/mobile/framework/domain_module";
|
|
81
|
+
import type { Subscriber } from "@sincpro/mobile/domain/event_sourcing";
|
|
60
82
|
import { setPrinterDriver } from "@sincpro/mobile/domain/print";
|
|
61
83
|
```
|
|
62
84
|
|
|
63
85
|
## Impresora (opcional)
|
|
64
86
|
|
|
65
|
-
El core es agnóstico del hardware: define el puerto `IPrinterDriver` (`@sincpro/mobile/domain/print`). La app registra un driver concreto al bootstrap
|
|
66
|
-
|
|
67
|
-
```ts
|
|
68
|
-
import { setPrinterDriver } from "@sincpro/mobile/domain/print";
|
|
69
|
-
setPrinterDriver(miDriver);
|
|
70
|
-
```
|
|
87
|
+
El core es agnóstico del hardware: define el puerto `IPrinterDriver` (`@sincpro/mobile/domain/print`). La app registra un driver concreto al bootstrap (vía `printerService.setDriver(...)`); si no registra ninguno, el driver Noop degrada con un warning.
|
|
71
88
|
|
|
72
89
|
## Desarrollo
|
|
73
90
|
|
|
74
|
-
Todo vía Makefile: `make
|
|
91
|
+
Todo vía Makefile. Dos comandos de calidad: `make format` (auto-fix: eslint --fix + prettier + typecheck) y `make verify-format` (gate de CI: corre `format` y falla si quedó algo sin formatear/commitear — cubre lint + formato + tipos). Además `make build` (tsc + tsc-alias → `dist/`) y `make publish`. Detalle del workflow y guardrails en [`AGENTS.md`](AGENTS.md).
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { DomainEvent, EEventStatus } from "../../domain/event_sourcing";
|
|
2
2
|
import { ECommonRepository } from "../../domain/repositories";
|
|
3
|
+
import { DATABASE_TABLES } from "../../entrypoints/db/migrations";
|
|
3
4
|
import { DBCursor } from "../../infrastructure/database";
|
|
4
5
|
import { loggerRepositories } from "../../infrastructure/logger";
|
|
5
6
|
class DomainEventRepositoryImpl {
|
|
6
7
|
name = ECommonRepository.DOMAIN_EVENT;
|
|
7
|
-
table =
|
|
8
|
+
table = DATABASE_TABLES.DOMAIN_EVENTS;
|
|
8
9
|
async hasPendingDuplicate(event) {
|
|
9
10
|
if (event.correlationId) {
|
|
10
11
|
const existing = await DBCursor.getFirstAsync(`SELECT uuid FROM ${this.table}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { DomainEvent } from "../../domain/event_sourcing";
|
|
2
2
|
import { ECommonRepository } from "../../domain/repositories";
|
|
3
|
+
import { DATABASE_TABLES } from "../../entrypoints/db/migrations";
|
|
3
4
|
import { DBCursor } from "../../infrastructure/database";
|
|
4
5
|
import { loggerRepositories } from "../../infrastructure/logger";
|
|
5
6
|
class DomainEventDeadLetterRepositoryImpl {
|
|
6
7
|
name = ECommonRepository.DOMAIN_EVENT_DEAD_LETTER;
|
|
7
|
-
table =
|
|
8
|
+
table = DATABASE_TABLES.DOMAIN_EVENTS_DEAD_LETTER;
|
|
8
9
|
async save(event, errorMessage) {
|
|
9
10
|
await DBCursor.mutateDatabase(`INSERT INTO ${this.table}
|
|
10
11
|
(uuid, name, label, data, requires_network, created_at, attempts, error_message, failed_at, aggregate_id, source_id, correlation_id, sequence)
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import { ECommonRepository } from "../../domain/repositories";
|
|
2
|
+
import { DATABASE_TABLES } from "../../entrypoints/db/migrations";
|
|
2
3
|
import { DBCursor } from "../../infrastructure/database";
|
|
3
4
|
class SettingsRepositoryImpl {
|
|
4
5
|
name = ECommonRepository.SETTINGS;
|
|
5
|
-
table =
|
|
6
|
+
table = DATABASE_TABLES.SETTINGS;
|
|
6
7
|
async saveOneSetting(name, value) {
|
|
7
8
|
const type = typeof value;
|
|
8
|
-
await DBCursor.mutateDatabase(`INSERT OR REPLACE INTO ${
|
|
9
|
+
await DBCursor.mutateDatabase(`INSERT OR REPLACE INTO ${DATABASE_TABLES.SETTINGS} (name, value, type)
|
|
9
10
|
VALUES (?, ?, ?)`, name, JSON.stringify(value), type);
|
|
10
11
|
}
|
|
11
12
|
async getSettingByName(name) {
|
|
12
13
|
const row = await DBCursor.getFirstAsync(`SELECT value, type
|
|
13
|
-
FROM ${
|
|
14
|
+
FROM ${DATABASE_TABLES.SETTINGS}
|
|
14
15
|
WHERE name = ?`, name);
|
|
15
16
|
if (!row) {
|
|
16
17
|
return null;
|
package/dist/domain/settings.js
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
import { DBCursor } from "../../infrastructure/database";
|
|
2
|
+
export var DATABASE_TABLES;
|
|
3
|
+
(function (DATABASE_TABLES) {
|
|
4
|
+
DATABASE_TABLES["SETTINGS"] = "settings";
|
|
5
|
+
DATABASE_TABLES["EVENT_QUEUE"] = "event_queue";
|
|
6
|
+
DATABASE_TABLES["DEAD_LETTER_QUEUE"] = "dead_letter_queue";
|
|
7
|
+
DATABASE_TABLES["DOMAIN_EVENTS"] = "domain_events";
|
|
8
|
+
DATABASE_TABLES["DOMAIN_EVENTS_DEAD_LETTER"] = "domain_events_dead_letter";
|
|
9
|
+
})(DATABASE_TABLES || (DATABASE_TABLES = {}));
|
|
2
10
|
async function createSettingsTable() {
|
|
3
11
|
await DBCursor.execAsync(`
|
|
4
|
-
CREATE TABLE IF NOT EXISTS ${
|
|
12
|
+
CREATE TABLE IF NOT EXISTS ${DATABASE_TABLES.SETTINGS}(
|
|
5
13
|
name TEXT PRIMARY KEY NOT NULL,
|
|
6
14
|
value TEXT NOT NULL,
|
|
7
15
|
type TEXT NOT NULL,
|
|
@@ -11,7 +19,7 @@ async function createSettingsTable() {
|
|
|
11
19
|
}
|
|
12
20
|
async function createDeadLetterQueueTable() {
|
|
13
21
|
await DBCursor.execAsync(`
|
|
14
|
-
CREATE TABLE IF NOT EXISTS ${
|
|
22
|
+
CREATE TABLE IF NOT EXISTS ${DATABASE_TABLES.DEAD_LETTER_QUEUE} (
|
|
15
23
|
uuid TEXT PRIMARY KEY NOT NULL, -- UUID v7 for chronological ordering
|
|
16
24
|
name TEXT NOT NULL,
|
|
17
25
|
payload TEXT NOT NULL,
|
|
@@ -23,7 +31,7 @@ async function createDeadLetterQueueTable() {
|
|
|
23
31
|
}
|
|
24
32
|
async function createEventQueueTable() {
|
|
25
33
|
await DBCursor.execAsync(`
|
|
26
|
-
CREATE TABLE IF NOT EXISTS ${
|
|
34
|
+
CREATE TABLE IF NOT EXISTS ${DATABASE_TABLES.EVENT_QUEUE}(
|
|
27
35
|
uuid TEXT PRIMARY KEY NOT NULL,
|
|
28
36
|
name TEXT NOT NULL,
|
|
29
37
|
payload TEXT NOT NULL,
|
|
@@ -40,7 +48,7 @@ async function createEventQueueTable() {
|
|
|
40
48
|
}
|
|
41
49
|
async function createDomainEventsTable() {
|
|
42
50
|
await DBCursor.execAsync(`
|
|
43
|
-
CREATE TABLE IF NOT EXISTS ${
|
|
51
|
+
CREATE TABLE IF NOT EXISTS ${DATABASE_TABLES.DOMAIN_EVENTS} (
|
|
44
52
|
uuid TEXT PRIMARY KEY NOT NULL,
|
|
45
53
|
name TEXT NOT NULL,
|
|
46
54
|
label TEXT NOT NULL,
|
|
@@ -59,24 +67,24 @@ async function createDomainEventsTable() {
|
|
|
59
67
|
);
|
|
60
68
|
`);
|
|
61
69
|
await DBCursor.execAsync(`
|
|
62
|
-
CREATE INDEX IF NOT EXISTS idx_domain_events_status ON ${
|
|
70
|
+
CREATE INDEX IF NOT EXISTS idx_domain_events_status ON ${DATABASE_TABLES.DOMAIN_EVENTS}(status);
|
|
63
71
|
`);
|
|
64
72
|
await DBCursor.execAsync(`
|
|
65
|
-
CREATE INDEX IF NOT EXISTS idx_domain_events_correlation ON ${
|
|
73
|
+
CREATE INDEX IF NOT EXISTS idx_domain_events_correlation ON ${DATABASE_TABLES.DOMAIN_EVENTS}(correlation_id);
|
|
66
74
|
`);
|
|
67
75
|
await DBCursor.execAsync(`
|
|
68
|
-
CREATE INDEX IF NOT EXISTS idx_domain_events_aggregate ON ${
|
|
76
|
+
CREATE INDEX IF NOT EXISTS idx_domain_events_aggregate ON ${DATABASE_TABLES.DOMAIN_EVENTS}(aggregate_id);
|
|
69
77
|
`);
|
|
70
78
|
await DBCursor.execAsync(`
|
|
71
|
-
CREATE INDEX IF NOT EXISTS idx_domain_events_name ON ${
|
|
79
|
+
CREATE INDEX IF NOT EXISTS idx_domain_events_name ON ${DATABASE_TABLES.DOMAIN_EVENTS}(name);
|
|
72
80
|
`);
|
|
73
81
|
await DBCursor.execAsync(`
|
|
74
|
-
CREATE INDEX IF NOT EXISTS idx_domain_events_created ON ${
|
|
82
|
+
CREATE INDEX IF NOT EXISTS idx_domain_events_created ON ${DATABASE_TABLES.DOMAIN_EVENTS}(created_at);
|
|
75
83
|
`);
|
|
76
84
|
}
|
|
77
85
|
async function createDomainEventsDeadLetterTable() {
|
|
78
86
|
await DBCursor.execAsync(`
|
|
79
|
-
CREATE TABLE IF NOT EXISTS ${
|
|
87
|
+
CREATE TABLE IF NOT EXISTS ${DATABASE_TABLES.DOMAIN_EVENTS_DEAD_LETTER} (
|
|
80
88
|
uuid TEXT PRIMARY KEY NOT NULL,
|
|
81
89
|
name TEXT NOT NULL,
|
|
82
90
|
label TEXT NOT NULL,
|
|
@@ -93,22 +101,22 @@ async function createDomainEventsDeadLetterTable() {
|
|
|
93
101
|
);
|
|
94
102
|
`);
|
|
95
103
|
await DBCursor.execAsync(`
|
|
96
|
-
CREATE INDEX IF NOT EXISTS idx_dead_letter_correlation ON ${
|
|
104
|
+
CREATE INDEX IF NOT EXISTS idx_dead_letter_correlation ON ${DATABASE_TABLES.DOMAIN_EVENTS_DEAD_LETTER}(correlation_id);
|
|
97
105
|
`);
|
|
98
106
|
await DBCursor.execAsync(`
|
|
99
|
-
CREATE INDEX IF NOT EXISTS idx_dead_letter_name ON ${
|
|
107
|
+
CREATE INDEX IF NOT EXISTS idx_dead_letter_name ON ${DATABASE_TABLES.DOMAIN_EVENTS_DEAD_LETTER}(name);
|
|
100
108
|
`);
|
|
101
109
|
await DBCursor.execAsync(`
|
|
102
|
-
CREATE INDEX IF NOT EXISTS idx_dead_letter_failed ON ${
|
|
110
|
+
CREATE INDEX IF NOT EXISTS idx_dead_letter_failed ON ${DATABASE_TABLES.DOMAIN_EVENTS_DEAD_LETTER}(failed_at);
|
|
103
111
|
`);
|
|
104
112
|
}
|
|
105
113
|
const MIGRATIONS = [
|
|
106
|
-
{ name:
|
|
107
|
-
{ name:
|
|
108
|
-
{ name:
|
|
109
|
-
{ name:
|
|
114
|
+
{ name: DATABASE_TABLES.SETTINGS, migrationFn: createSettingsTable },
|
|
115
|
+
{ name: DATABASE_TABLES.EVENT_QUEUE, migrationFn: createEventQueueTable },
|
|
116
|
+
{ name: DATABASE_TABLES.DEAD_LETTER_QUEUE, migrationFn: createDeadLetterQueueTable },
|
|
117
|
+
{ name: DATABASE_TABLES.DOMAIN_EVENTS, migrationFn: createDomainEventsTable },
|
|
110
118
|
{
|
|
111
|
-
name:
|
|
119
|
+
name: DATABASE_TABLES.DOMAIN_EVENTS_DEAD_LETTER,
|
|
112
120
|
migrationFn: createDomainEventsDeadLetterTable,
|
|
113
121
|
},
|
|
114
122
|
];
|
|
@@ -6,9 +6,9 @@ export class PrintImageSubscriber extends Subscriber {
|
|
|
6
6
|
requiresAuth = false;
|
|
7
7
|
listen = [PrintImageRequestedEvent];
|
|
8
8
|
async process(event) {
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
const printed = await printerService.printImageBase64(event.imageBase64);
|
|
10
|
+
if (!printed) {
|
|
11
|
+
throw new DomainValidationError("Print failed — printer not connected or timed out");
|
|
11
12
|
}
|
|
12
|
-
await printerService.printImageBase64(event.imageBase64);
|
|
13
13
|
}
|
|
14
14
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { DomainModule } from "../../framework/domain_module";
|
|
2
|
+
import { type BrandingConfig } from "@sincpro/mobile-ui";
|
|
2
3
|
import { type ThemeTokens } from "@sincpro/mobile-ui/theme";
|
|
3
4
|
import { type ComponentType, type ReactNode } from "react";
|
|
4
5
|
type ProviderComponent = ComponentType<{
|
|
@@ -10,6 +11,12 @@ export interface AppShellConfig {
|
|
|
10
11
|
activeDomain: string;
|
|
11
12
|
providers?: ProviderComponent[];
|
|
12
13
|
theme?: ThemeTokens;
|
|
14
|
+
darkTheme?: ThemeTokens;
|
|
15
|
+
branding?: BrandingConfig;
|
|
16
|
+
/** Component rendered while fonts + domain init complete. Use `AppSplashView` from the DS or roll your own. */
|
|
17
|
+
splashComponent?: ComponentType;
|
|
18
|
+
/** Minimum time (ms) the splash stays visible. Default 2000. */
|
|
19
|
+
splashDuration?: number;
|
|
13
20
|
}
|
|
14
21
|
export declare function createAppShell(config: AppShellConfig): ComponentType;
|
|
15
22
|
export {};
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "nativewind/jsx-runtime";
|
|
2
|
+
import { DefaultTheme as NavDefaultTheme, NavigationContainer, } from "@react-navigation/native";
|
|
2
3
|
import { createApp } from "../../framework/createApp";
|
|
3
4
|
import { ToastHost } from "../../infrastructure/ui/ToastHost";
|
|
4
|
-
import {
|
|
5
|
+
import { setBranding } from "@sincpro/mobile-ui";
|
|
6
|
+
import { ToastProvider } from "@sincpro/mobile-ui/Feedback";
|
|
7
|
+
import { setActiveTheme, themeToVars, useTheme, } from "@sincpro/mobile-ui/theme";
|
|
5
8
|
import { useAppFonts } from "@sincpro/mobile-ui/theme/typography";
|
|
6
9
|
import * as SplashScreen from "expo-splash-screen";
|
|
7
10
|
import { vars } from "nativewind";
|
|
@@ -9,8 +12,7 @@ import { useEffect, useState } from "react";
|
|
|
9
12
|
import { View } from "react-native";
|
|
10
13
|
import { GestureHandlerRootView } from "react-native-gesture-handler";
|
|
11
14
|
import { KeyboardProvider } from "react-native-keyboard-controller";
|
|
12
|
-
import { initialWindowMetrics, SafeAreaProvider } from "react-native-safe-area-context";
|
|
13
|
-
import { NativeRouter } from "react-router-native";
|
|
15
|
+
import { initialWindowMetrics, SafeAreaProvider, SafeAreaView, } from "react-native-safe-area-context";
|
|
14
16
|
import { CommonProvider } from "./common_provider";
|
|
15
17
|
import { ActiveDomainApp, DomainSwitcherProvider } from "./domain_switcher";
|
|
16
18
|
function withProviders(providers, children) {
|
|
@@ -28,10 +30,19 @@ export function createAppShell(config) {
|
|
|
28
30
|
if (config.theme) {
|
|
29
31
|
setActiveTheme(config.theme);
|
|
30
32
|
}
|
|
31
|
-
|
|
33
|
+
if (config.branding) {
|
|
34
|
+
setBranding(config.branding);
|
|
35
|
+
}
|
|
32
36
|
return function AppShell() {
|
|
33
37
|
const [isReady, setIsReady] = useState(false);
|
|
38
|
+
const [minDurationMet, setMinDurationMet] = useState(false);
|
|
34
39
|
const fontsLoaded = useAppFonts();
|
|
40
|
+
const activeTheme = useTheme();
|
|
41
|
+
const themeStyle = vars(themeToVars(activeTheme));
|
|
42
|
+
const navTheme = {
|
|
43
|
+
...NavDefaultTheme,
|
|
44
|
+
colors: { ...NavDefaultTheme.colors, background: activeTheme.bg.page },
|
|
45
|
+
};
|
|
35
46
|
useEffect(() => {
|
|
36
47
|
let active = true;
|
|
37
48
|
createApp({ domains: config.domains }).then(() => {
|
|
@@ -46,13 +57,18 @@ export function createAppShell(config) {
|
|
|
46
57
|
SplashScreen.preventAutoHideAsync().catch(() => { });
|
|
47
58
|
}, []);
|
|
48
59
|
useEffect(() => {
|
|
49
|
-
|
|
60
|
+
const t = setTimeout(() => setMinDurationMet(true), config.splashDuration ?? 2000);
|
|
61
|
+
return () => clearTimeout(t);
|
|
62
|
+
}, []);
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
if (fontsLoaded && isReady && minDurationMet) {
|
|
50
65
|
SplashScreen.hideAsync().catch(() => { });
|
|
51
66
|
}
|
|
52
|
-
}, [fontsLoaded, isReady]);
|
|
53
|
-
if (!isReady || !fontsLoaded) {
|
|
54
|
-
|
|
67
|
+
}, [fontsLoaded, isReady, minDurationMet]);
|
|
68
|
+
if (!isReady || !fontsLoaded || !minDurationMet) {
|
|
69
|
+
const Splash = config.splashComponent;
|
|
70
|
+
return Splash ? (_jsx(Splash, {})) : (_jsx(View, { style: { flex: 1, backgroundColor: activeTheme.bg.page } }));
|
|
55
71
|
}
|
|
56
|
-
return (_jsx(GestureHandlerRootView, { style: { flex: 1 }, children: _jsx(View, { className: "flex-1", style: themeStyle, children: _jsx(SafeAreaProvider, { initialMetrics: initialWindowMetrics, children: _jsx(KeyboardProvider, { children:
|
|
72
|
+
return (_jsx(GestureHandlerRootView, { style: { flex: 1 }, children: _jsx(View, { className: "flex-1", style: themeStyle, children: _jsx(SafeAreaProvider, { initialMetrics: initialWindowMetrics, children: _jsx(SafeAreaView, { edges: ["top"], style: { flex: 1 }, children: _jsx(KeyboardProvider, { children: _jsx(CommonProvider, { darkTheme: config.darkTheme, lightTheme: config.theme, children: _jsxs(ToastProvider, { position: "top", children: [withProviders(providers, _jsx(DomainSwitcherProvider, { apps: apps, initialDomain: config.activeDomain, children: _jsx(NavigationContainer, { theme: navTheme, children: _jsx(ActiveDomainApp, {}) }) })), _jsx(ToastHost, {})] }) }) }) }) }) }) }));
|
|
57
73
|
};
|
|
58
74
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ThemeTokens } from "@sincpro/mobile-ui/theme";
|
|
1
2
|
import { ReactNode } from "react";
|
|
2
3
|
export interface TimezoneLocale {
|
|
3
4
|
timezone: string;
|
|
@@ -26,10 +27,14 @@ interface ICommonContext {
|
|
|
26
27
|
requestGeoPermission: () => Promise<boolean>;
|
|
27
28
|
timezone: string | null;
|
|
28
29
|
updateTimezone: (tz: TimezoneLocale) => Promise<void>;
|
|
30
|
+
colorScheme: "light" | "dark" | "system";
|
|
31
|
+
setColorScheme: (scheme: "light" | "dark" | "system") => Promise<void>;
|
|
29
32
|
}
|
|
30
33
|
interface CommonProviderProps {
|
|
31
34
|
children: ReactNode;
|
|
35
|
+
lightTheme?: ThemeTokens;
|
|
36
|
+
darkTheme?: ThemeTokens;
|
|
32
37
|
}
|
|
33
|
-
export declare function CommonProvider({ children }: CommonProviderProps): import("react/jsx-runtime").JSX.Element;
|
|
38
|
+
export declare function CommonProvider({ children, lightTheme, darkTheme }: CommonProviderProps): import("react/jsx-runtime").JSX.Element;
|
|
34
39
|
export declare function useCommon(): ICommonContext;
|
|
35
40
|
export {};
|
|
@@ -4,13 +4,16 @@ import { SettingsRepository } from "../../adapters/repositories/setting.reposito
|
|
|
4
4
|
import { QueueEndEvent, QueueStartEvent } from "../../domain/events";
|
|
5
5
|
import logger, { loggerUseCases } from "../../infrastructure/logger";
|
|
6
6
|
import { UIEventBus } from "../../infrastructure/ui/UIEventBus";
|
|
7
|
+
import { DEFAULT_DARK_THEME, DEFAULT_THEME, setActiveTheme } from "@sincpro/mobile-ui/theme";
|
|
7
8
|
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, } from "react";
|
|
9
|
+
import { Appearance, AppState } from "react-native";
|
|
8
10
|
const DEBUG_MODE_SETTING_KEY = "app.debug_mode";
|
|
9
11
|
const TIMEZONE_SETTING_KEY = "app.timezone";
|
|
10
12
|
const LOCALE_SETTING_KEY = "app.locale";
|
|
13
|
+
const COLOR_SCHEME_SETTING_KEY = "app.color_scheme";
|
|
11
14
|
const QUEUE_MIN_VISIBLE_MS = 1200;
|
|
12
15
|
const CommonContext = createContext(null);
|
|
13
|
-
export function CommonProvider({ children }) {
|
|
16
|
+
export function CommonProvider({ children, lightTheme, darkTheme }) {
|
|
14
17
|
const [debugMode, setDebugMode] = useState(false);
|
|
15
18
|
const [debugModeLoaded, setDebugModeLoaded] = useState(false);
|
|
16
19
|
const [currentActivity, setCurrentActivity] = useState(undefined);
|
|
@@ -21,6 +24,7 @@ export function CommonProvider({ children }) {
|
|
|
21
24
|
const [geoIsLoading, setGeoIsLoading] = useState(false);
|
|
22
25
|
const [geoError, setGeoError] = useState(null);
|
|
23
26
|
const [timezone, setTimezone] = useState(null);
|
|
27
|
+
const [colorScheme, setColorSchemeState] = useState("system");
|
|
24
28
|
const errorTimeoutRef = useRef(null);
|
|
25
29
|
const queueClearTimeoutRef = useRef(null);
|
|
26
30
|
useEffect(() => {
|
|
@@ -86,6 +90,42 @@ export function CommonProvider({ children }) {
|
|
|
86
90
|
logger.warn(error);
|
|
87
91
|
}
|
|
88
92
|
}, []);
|
|
93
|
+
const applyTheme = useCallback((scheme) => {
|
|
94
|
+
const resolved = scheme === "system" ? (Appearance.getColorScheme() ?? "light") : scheme;
|
|
95
|
+
setActiveTheme(resolved === "dark"
|
|
96
|
+
? (darkTheme ?? DEFAULT_DARK_THEME)
|
|
97
|
+
: (lightTheme ?? DEFAULT_THEME));
|
|
98
|
+
}, [lightTheme, darkTheme]);
|
|
99
|
+
const setColorScheme = useCallback(async (scheme) => {
|
|
100
|
+
await SettingsRepository.saveOneSetting(COLOR_SCHEME_SETTING_KEY, scheme);
|
|
101
|
+
setColorSchemeState(scheme);
|
|
102
|
+
applyTheme(scheme);
|
|
103
|
+
}, [applyTheme]);
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
checkGeoPermission();
|
|
106
|
+
const sub = AppState.addEventListener("change", (state) => {
|
|
107
|
+
if (state === "active")
|
|
108
|
+
checkGeoPermission();
|
|
109
|
+
});
|
|
110
|
+
return () => sub.remove();
|
|
111
|
+
}, [checkGeoPermission]);
|
|
112
|
+
// Load persisted color scheme on mount
|
|
113
|
+
useEffect(() => {
|
|
114
|
+
async function loadColorScheme() {
|
|
115
|
+
const saved = await SettingsRepository.getSettingByName(COLOR_SCHEME_SETTING_KEY);
|
|
116
|
+
const scheme = saved === "light" || saved === "dark" || saved === "system" ? saved : "system";
|
|
117
|
+
setColorSchemeState(scheme);
|
|
118
|
+
applyTheme(scheme);
|
|
119
|
+
}
|
|
120
|
+
loadColorScheme();
|
|
121
|
+
}, [applyTheme]);
|
|
122
|
+
// Follow system changes when scheme is "system"
|
|
123
|
+
useEffect(() => {
|
|
124
|
+
if (colorScheme !== "system")
|
|
125
|
+
return;
|
|
126
|
+
const sub = Appearance.addChangeListener(() => applyTheme("system"));
|
|
127
|
+
return () => sub.remove();
|
|
128
|
+
}, [colorScheme, applyTheme]);
|
|
89
129
|
useEffect(() => {
|
|
90
130
|
async function loadTimezone() {
|
|
91
131
|
try {
|
|
@@ -217,6 +257,8 @@ export function CommonProvider({ children }) {
|
|
|
217
257
|
requestGeoPermission,
|
|
218
258
|
timezone,
|
|
219
259
|
updateTimezone,
|
|
260
|
+
colorScheme,
|
|
261
|
+
setColorScheme,
|
|
220
262
|
}), [
|
|
221
263
|
debugMode,
|
|
222
264
|
debugModeLoaded,
|
|
@@ -232,6 +274,8 @@ export function CommonProvider({ children }) {
|
|
|
232
274
|
requestGeoPermission,
|
|
233
275
|
timezone,
|
|
234
276
|
updateTimezone,
|
|
277
|
+
colorScheme,
|
|
278
|
+
setColorScheme,
|
|
235
279
|
]);
|
|
236
280
|
return _jsx(CommonContext.Provider, { value: value, children: children });
|
|
237
281
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx } from "nativewind/jsx-runtime";
|
|
2
|
+
import { createNativeStackNavigator } from "@react-navigation/native-stack";
|
|
2
3
|
import { createContext, useContext, useState, } from "react";
|
|
4
|
+
const RootStack = createNativeStackNavigator();
|
|
3
5
|
const DomainSwitcherContext = createContext(null);
|
|
4
6
|
export function DomainSwitcherProvider(props) {
|
|
5
7
|
const [activeDomain, setActiveDomain] = useState(props.initialDomain);
|
|
@@ -14,9 +16,8 @@ export function useDomainSwitcher() {
|
|
|
14
16
|
}
|
|
15
17
|
export function ActiveDomainApp() {
|
|
16
18
|
const { activeDomain, apps } = useDomainSwitcher();
|
|
17
|
-
const
|
|
18
|
-
if (!
|
|
19
|
+
const currentApp = apps.find((entry) => entry.key === activeDomain);
|
|
20
|
+
if (!currentApp)
|
|
19
21
|
return null;
|
|
20
|
-
|
|
21
|
-
return _jsx(Component, {});
|
|
22
|
+
return (_jsx(RootStack.Navigator, { screenOptions: { headerShown: false }, children: _jsx(RootStack.Screen, { component: currentApp.component, name: currentApp.key }) }));
|
|
22
23
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -13,4 +13,3 @@ export { DomainModule } from "./framework/domain_module";
|
|
|
13
13
|
export { Kernel } from "./framework/kernel";
|
|
14
14
|
export { orchestrator } from "./framework/orchestrator";
|
|
15
15
|
export { installGlobalErrorHandler } from "./infrastructure/ui/errorHandler";
|
|
16
|
-
export { PlainLayout, TabNavigatorLayout } from "./ui/layouts/router_layouts";
|
package/dist/index.js
CHANGED
|
@@ -8,4 +8,3 @@ export { DomainModule } from "./framework/domain_module";
|
|
|
8
8
|
export { Kernel } from "./framework/kernel";
|
|
9
9
|
export { orchestrator } from "./framework/orchestrator";
|
|
10
10
|
export { installGlobalErrorHandler } from "./infrastructure/ui/errorHandler";
|
|
11
|
-
export { PlainLayout, TabNavigatorLayout } from "./ui/layouts/router_layouts";
|
|
@@ -5,13 +5,13 @@ interface ILogger {
|
|
|
5
5
|
warn(...args: LoggerArgs): void;
|
|
6
6
|
error(...args: LoggerArgs): void;
|
|
7
7
|
}
|
|
8
|
-
declare
|
|
8
|
+
declare enum ELogLevel {
|
|
9
9
|
DEBUG = "DEBUG",
|
|
10
10
|
INFO = "INFO",
|
|
11
11
|
WARN = "WARN",
|
|
12
12
|
ERROR = "ERROR"
|
|
13
13
|
}
|
|
14
|
-
declare
|
|
14
|
+
declare enum EApplicationLogger {
|
|
15
15
|
GLOBAL = "GLOBAL",
|
|
16
16
|
ODOO_CLIENT = "ODOO_CLIENT",
|
|
17
17
|
ADAPTER = "ADAPTER",
|