@nextsparkjs/mobile 0.1.0-beta.1

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.
Files changed (123) hide show
  1. package/README.md +339 -0
  2. package/dist/api/client.d.ts +102 -0
  3. package/dist/api/client.js +189 -0
  4. package/dist/api/client.js.map +1 -0
  5. package/dist/api/client.types.d.ts +39 -0
  6. package/dist/api/client.types.js +12 -0
  7. package/dist/api/client.types.js.map +1 -0
  8. package/dist/api/core/auth.d.ts +26 -0
  9. package/dist/api/core/auth.js +52 -0
  10. package/dist/api/core/auth.js.map +1 -0
  11. package/dist/api/core/index.d.ts +4 -0
  12. package/dist/api/core/index.js +5 -0
  13. package/dist/api/core/index.js.map +1 -0
  14. package/dist/api/core/teams.d.ts +20 -0
  15. package/dist/api/core/teams.js +19 -0
  16. package/dist/api/core/teams.js.map +1 -0
  17. package/dist/api/core/types.d.ts +58 -0
  18. package/dist/api/core/types.js +1 -0
  19. package/dist/api/core/types.js.map +1 -0
  20. package/dist/api/core/users.d.ts +43 -0
  21. package/dist/api/core/users.js +41 -0
  22. package/dist/api/core/users.js.map +1 -0
  23. package/dist/api/entities/factory.d.ts +43 -0
  24. package/dist/api/entities/factory.js +31 -0
  25. package/dist/api/entities/factory.js.map +1 -0
  26. package/dist/api/entities/index.d.ts +3 -0
  27. package/dist/api/entities/index.js +3 -0
  28. package/dist/api/entities/index.js.map +1 -0
  29. package/dist/api/entities/types.d.ts +32 -0
  30. package/dist/api/entities/types.js +1 -0
  31. package/dist/api/entities/types.js.map +1 -0
  32. package/dist/api/index.d.ts +7 -0
  33. package/dist/api/index.js +15 -0
  34. package/dist/api/index.js.map +1 -0
  35. package/dist/hooks/index.d.ts +4 -0
  36. package/dist/hooks/index.js +5 -0
  37. package/dist/hooks/index.js.map +1 -0
  38. package/dist/index.d.ts +14 -0
  39. package/dist/index.js +28 -0
  40. package/dist/index.js.map +1 -0
  41. package/dist/lib/alert.d.ts +34 -0
  42. package/dist/lib/alert.js +73 -0
  43. package/dist/lib/alert.js.map +1 -0
  44. package/dist/lib/index.d.ts +2 -0
  45. package/dist/lib/index.js +10 -0
  46. package/dist/lib/index.js.map +1 -0
  47. package/dist/lib/storage.d.ts +1 -0
  48. package/dist/lib/storage.js +29 -0
  49. package/dist/lib/storage.js.map +1 -0
  50. package/dist/providers/AuthProvider.d.ts +21 -0
  51. package/dist/providers/AuthProvider.js +113 -0
  52. package/dist/providers/AuthProvider.js.map +1 -0
  53. package/dist/providers/QueryProvider.d.ts +11 -0
  54. package/dist/providers/QueryProvider.js +23 -0
  55. package/dist/providers/QueryProvider.js.map +1 -0
  56. package/dist/providers/index.d.ts +6 -0
  57. package/dist/providers/index.js +9 -0
  58. package/dist/providers/index.js.map +1 -0
  59. package/dist/storage-BaRppHUz.d.ts +22 -0
  60. package/package.json +99 -0
  61. package/templates/app/(app)/_layout.tsx +216 -0
  62. package/templates/app/(app)/customer/[id].tsx +68 -0
  63. package/templates/app/(app)/customer/create.tsx +24 -0
  64. package/templates/app/(app)/customers.tsx +164 -0
  65. package/templates/app/(app)/index.tsx +310 -0
  66. package/templates/app/(app)/notifications.tsx +242 -0
  67. package/templates/app/(app)/profile.tsx +254 -0
  68. package/templates/app/(app)/settings.tsx +241 -0
  69. package/templates/app/(app)/task/[id].tsx +70 -0
  70. package/templates/app/(app)/task/create.tsx +24 -0
  71. package/templates/app/(app)/tasks.tsx +164 -0
  72. package/templates/app/_layout.tsx +54 -0
  73. package/templates/app/index.tsx +35 -0
  74. package/templates/app/login.tsx +179 -0
  75. package/templates/app.config.ts +39 -0
  76. package/templates/babel.config.js +9 -0
  77. package/templates/eas.json +18 -0
  78. package/templates/jest.config.js +12 -0
  79. package/templates/metro.config.js +23 -0
  80. package/templates/package.json.template +52 -0
  81. package/templates/src/components/entities/customers/CustomerCard.tsx +59 -0
  82. package/templates/src/components/entities/customers/CustomerForm.tsx +194 -0
  83. package/templates/src/components/entities/customers/index.ts +6 -0
  84. package/templates/src/components/entities/index.ts +9 -0
  85. package/templates/src/components/entities/tasks/TaskCard.tsx +89 -0
  86. package/templates/src/components/entities/tasks/TaskForm.tsx +231 -0
  87. package/templates/src/components/entities/tasks/index.ts +6 -0
  88. package/templates/src/components/features/index.ts +6 -0
  89. package/templates/src/components/navigation/BottomTabBar.tsx +80 -0
  90. package/templates/src/components/navigation/CreateSheet.tsx +108 -0
  91. package/templates/src/components/navigation/MoreSheet.tsx +403 -0
  92. package/templates/src/components/navigation/TopBar.tsx +74 -0
  93. package/templates/src/components/navigation/index.ts +8 -0
  94. package/templates/src/components/ui/index.ts +89 -0
  95. package/templates/src/components/ui/text.tsx +64 -0
  96. package/templates/src/config/api.config.ts +26 -0
  97. package/templates/src/config/app.config.ts +15 -0
  98. package/templates/src/config/hooks.ts +58 -0
  99. package/templates/src/config/permissions.config.ts +119 -0
  100. package/templates/src/constants/colors.ts +55 -0
  101. package/templates/src/data/notifications.mock.json +100 -0
  102. package/templates/src/entities/customers/api.ts +10 -0
  103. package/templates/src/entities/customers/constants.internal.ts +6 -0
  104. package/templates/src/entities/customers/constants.ts +14 -0
  105. package/templates/src/entities/customers/index.ts +9 -0
  106. package/templates/src/entities/customers/mutations.ts +58 -0
  107. package/templates/src/entities/customers/queries.ts +40 -0
  108. package/templates/src/entities/customers/types.ts +43 -0
  109. package/templates/src/entities/index.ts +8 -0
  110. package/templates/src/entities/tasks/api.ts +10 -0
  111. package/templates/src/entities/tasks/constants.internal.ts +6 -0
  112. package/templates/src/entities/tasks/constants.ts +39 -0
  113. package/templates/src/entities/tasks/index.ts +9 -0
  114. package/templates/src/entities/tasks/mutations.ts +108 -0
  115. package/templates/src/entities/tasks/queries.ts +42 -0
  116. package/templates/src/entities/tasks/types.ts +52 -0
  117. package/templates/src/hooks/useCustomers.ts +17 -0
  118. package/templates/src/hooks/useTasks.ts +18 -0
  119. package/templates/src/lib/utils.ts +10 -0
  120. package/templates/src/styles/globals.css +103 -0
  121. package/templates/src/types/index.ts +45 -0
  122. package/templates/tailwind.config.js +108 -0
  123. package/templates/tsconfig.json +15 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/lib/alert.ts"],"sourcesContent":["/**\n * Cross-platform Alert utility\n * Works on iOS, Android, and Web\n */\n\nimport { Alert as RNAlert, Platform } from \"react-native\";\n\ninterface AlertButton {\n text: string;\n style?: \"default\" | \"cancel\" | \"destructive\";\n onPress?: () => void;\n}\n\ninterface AlertOptions {\n title: string;\n message?: string;\n buttons?: AlertButton[];\n}\n\n/**\n * Show an alert dialog that works on all platforms\n */\nexport function alert(options: AlertOptions): void {\n const { title, message, buttons = [{ text: \"OK\" }] } = options;\n\n if (Platform.OS === \"web\") {\n // Web: use window.confirm for simple confirm dialogs\n const hasDestructive = buttons.some((b) => b.style === \"destructive\");\n const hasCancel = buttons.some((b) => b.style === \"cancel\");\n\n if (hasDestructive && hasCancel) {\n // Confirmation dialog\n const confirmed = window.confirm(`${title}\\n\\n${message || \"\"}`);\n if (confirmed) {\n const destructiveBtn = buttons.find((b) => b.style === \"destructive\");\n destructiveBtn?.onPress?.();\n } else {\n const cancelBtn = buttons.find((b) => b.style === \"cancel\");\n cancelBtn?.onPress?.();\n }\n } else if (buttons.length === 1) {\n // Simple alert\n window.alert(`${title}\\n\\n${message || \"\"}`);\n buttons[0]?.onPress?.();\n } else {\n // For more complex cases, use confirm\n const confirmed = window.confirm(`${title}\\n\\n${message || \"\"}`);\n const btn = confirmed ? buttons[buttons.length - 1] : buttons[0];\n btn?.onPress?.();\n }\n } else {\n // Native: use React Native Alert\n RNAlert.alert(title, message, buttons);\n }\n}\n\n/**\n * Show a confirmation dialog\n * Returns a promise that resolves to true if confirmed, false if cancelled\n */\nexport function confirm(title: string, message?: string): Promise<boolean> {\n return new Promise((resolve) => {\n alert({\n title,\n message,\n buttons: [\n { text: \"Cancelar\", style: \"cancel\", onPress: () => resolve(false) },\n { text: \"Confirmar\", style: \"default\", onPress: () => resolve(true) },\n ],\n });\n });\n}\n\n/**\n * Show a destructive confirmation dialog (for delete actions)\n */\nexport function confirmDestructive(\n title: string,\n message?: string,\n destructiveButtonText = \"Eliminar\"\n): Promise<boolean> {\n return new Promise((resolve) => {\n alert({\n title,\n message,\n buttons: [\n { text: \"Cancelar\", style: \"cancel\", onPress: () => resolve(false) },\n {\n text: destructiveButtonText,\n style: \"destructive\",\n onPress: () => resolve(true),\n },\n ],\n });\n });\n}\n\nexport const Alert = {\n alert,\n confirm,\n confirmDestructive,\n};\n"],"mappings":"AAKA,SAAS,SAAS,SAAS,gBAAgB;AAiBpC,SAAS,MAAM,SAA6B;AACjD,QAAM,EAAE,OAAO,SAAS,UAAU,CAAC,EAAE,MAAM,KAAK,CAAC,EAAE,IAAI;AAEvD,MAAI,SAAS,OAAO,OAAO;AAEzB,UAAM,iBAAiB,QAAQ,KAAK,CAAC,MAAM,EAAE,UAAU,aAAa;AACpE,UAAM,YAAY,QAAQ,KAAK,CAAC,MAAM,EAAE,UAAU,QAAQ;AAE1D,QAAI,kBAAkB,WAAW;AAE/B,YAAM,YAAY,OAAO,QAAQ,GAAG,KAAK;AAAA;AAAA,EAAO,WAAW,EAAE,EAAE;AAC/D,UAAI,WAAW;AACb,cAAM,iBAAiB,QAAQ,KAAK,CAAC,MAAM,EAAE,UAAU,aAAa;AACpE,wBAAgB,UAAU;AAAA,MAC5B,OAAO;AACL,cAAM,YAAY,QAAQ,KAAK,CAAC,MAAM,EAAE,UAAU,QAAQ;AAC1D,mBAAW,UAAU;AAAA,MACvB;AAAA,IACF,WAAW,QAAQ,WAAW,GAAG;AAE/B,aAAO,MAAM,GAAG,KAAK;AAAA;AAAA,EAAO,WAAW,EAAE,EAAE;AAC3C,cAAQ,CAAC,GAAG,UAAU;AAAA,IACxB,OAAO;AAEL,YAAM,YAAY,OAAO,QAAQ,GAAG,KAAK;AAAA;AAAA,EAAO,WAAW,EAAE,EAAE;AAC/D,YAAM,MAAM,YAAY,QAAQ,QAAQ,SAAS,CAAC,IAAI,QAAQ,CAAC;AAC/D,WAAK,UAAU;AAAA,IACjB;AAAA,EACF,OAAO;AAEL,YAAQ,MAAM,OAAO,SAAS,OAAO;AAAA,EACvC;AACF;AAMO,SAAS,QAAQ,OAAe,SAAoC;AACzE,SAAO,IAAI,QAAQ,CAAC,YAAY;AAC9B,UAAM;AAAA,MACJ;AAAA,MACA;AAAA,MACA,SAAS;AAAA,QACP,EAAE,MAAM,YAAY,OAAO,UAAU,SAAS,MAAM,QAAQ,KAAK,EAAE;AAAA,QACnE,EAAE,MAAM,aAAa,OAAO,WAAW,SAAS,MAAM,QAAQ,IAAI,EAAE;AAAA,MACtE;AAAA,IACF,CAAC;AAAA,EACH,CAAC;AACH;AAKO,SAAS,mBACd,OACA,SACA,wBAAwB,YACN;AAClB,SAAO,IAAI,QAAQ,CAAC,YAAY;AAC9B,UAAM;AAAA,MACJ;AAAA,MACA;AAAA,MACA,SAAS;AAAA,QACP,EAAE,MAAM,YAAY,OAAO,UAAU,SAAS,MAAM,QAAQ,KAAK,EAAE;AAAA,QACnE;AAAA,UACE,MAAM;AAAA,UACN,OAAO;AAAA,UACP,SAAS,MAAM,QAAQ,IAAI;AAAA,QAC7B;AAAA,MACF;AAAA,IACF,CAAC;AAAA,EACH,CAAC;AACH;AAEO,MAAM,QAAQ;AAAA,EACnB;AAAA,EACA;AAAA,EACA;AACF;","names":[]}
@@ -0,0 +1,2 @@
1
+ export { s as Storage } from '../storage-BaRppHUz.js';
2
+ export { Alert, alert, confirm, confirmDestructive } from './alert.js';
@@ -0,0 +1,10 @@
1
+ import * as Storage from './storage.js';
2
+ import { alert, confirm, confirmDestructive, Alert } from './alert.js';
3
+ export {
4
+ Alert,
5
+ Storage,
6
+ alert,
7
+ confirm,
8
+ confirmDestructive
9
+ };
10
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/lib/index.ts"],"sourcesContent":["/**\n * Library utilities\n */\n\nexport * as Storage from './storage'\nexport { alert, confirm, confirmDestructive, Alert } from './alert'\n"],"mappings":"AAIA,YAAY,aAAa;AACzB,SAAS,OAAO,SAAS,oBAAoB,aAAa;","names":[]}
@@ -0,0 +1 @@
1
+ export { d as deleteItemAsync, g as getItemAsync, a as setItemAsync } from '../storage-BaRppHUz.js';
@@ -0,0 +1,29 @@
1
+ import { Platform } from "react-native";
2
+ import * as SecureStore from "expo-secure-store";
3
+ const isWeb = Platform.OS === "web";
4
+ async function getItemAsync(key) {
5
+ if (isWeb) {
6
+ return localStorage.getItem(key);
7
+ }
8
+ return SecureStore.getItemAsync(key);
9
+ }
10
+ async function setItemAsync(key, value) {
11
+ if (isWeb) {
12
+ localStorage.setItem(key, value);
13
+ return;
14
+ }
15
+ return SecureStore.setItemAsync(key, value);
16
+ }
17
+ async function deleteItemAsync(key) {
18
+ if (isWeb) {
19
+ localStorage.removeItem(key);
20
+ return;
21
+ }
22
+ return SecureStore.deleteItemAsync(key);
23
+ }
24
+ export {
25
+ deleteItemAsync,
26
+ getItemAsync,
27
+ setItemAsync
28
+ };
29
+ //# sourceMappingURL=storage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/lib/storage.ts"],"sourcesContent":["/**\n * Cross-platform secure storage\n * Uses expo-secure-store on native, localStorage on web\n *\n * ⚠️ SECURITY NOTE: Web storage uses localStorage which is vulnerable to XSS attacks.\n * For production web deployments, consider:\n * - Using httpOnly cookies for session management (requires backend changes)\n * - Implementing Content Security Policy (CSP)\n * - This is acceptable for development/testing and native mobile apps\n */\n\nimport { Platform } from 'react-native'\nimport * as SecureStore from 'expo-secure-store'\n\nconst isWeb = Platform.OS === 'web'\n\nexport async function getItemAsync(key: string): Promise<string | null> {\n if (isWeb) {\n return localStorage.getItem(key)\n }\n return SecureStore.getItemAsync(key)\n}\n\nexport async function setItemAsync(key: string, value: string): Promise<void> {\n if (isWeb) {\n localStorage.setItem(key, value)\n return\n }\n return SecureStore.setItemAsync(key, value)\n}\n\nexport async function deleteItemAsync(key: string): Promise<void> {\n if (isWeb) {\n localStorage.removeItem(key)\n return\n }\n return SecureStore.deleteItemAsync(key)\n}\n"],"mappings":"AAWA,SAAS,gBAAgB;AACzB,YAAY,iBAAiB;AAE7B,MAAM,QAAQ,SAAS,OAAO;AAE9B,eAAsB,aAAa,KAAqC;AACtE,MAAI,OAAO;AACT,WAAO,aAAa,QAAQ,GAAG;AAAA,EACjC;AACA,SAAO,YAAY,aAAa,GAAG;AACrC;AAEA,eAAsB,aAAa,KAAa,OAA8B;AAC5E,MAAI,OAAO;AACT,iBAAa,QAAQ,KAAK,KAAK;AAC/B;AAAA,EACF;AACA,SAAO,YAAY,aAAa,KAAK,KAAK;AAC5C;AAEA,eAAsB,gBAAgB,KAA4B;AAChE,MAAI,OAAO;AACT,iBAAa,WAAW,GAAG;AAC3B;AAAA,EACF;AACA,SAAO,YAAY,gBAAgB,GAAG;AACxC;","names":[]}
@@ -0,0 +1,21 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { ReactNode } from 'react';
3
+ import { User, Team } from '../api/core/types.js';
4
+
5
+ interface AuthContextValue {
6
+ user: User | null;
7
+ team: Team | null;
8
+ teams: Team[];
9
+ isLoading: boolean;
10
+ isAuthenticated: boolean;
11
+ login: (email: string, password: string) => Promise<void>;
12
+ logout: () => Promise<void>;
13
+ selectTeam: (team: Team) => Promise<void>;
14
+ }
15
+ interface AuthProviderProps {
16
+ children: ReactNode;
17
+ }
18
+ declare function AuthProvider({ children }: AuthProviderProps): react_jsx_runtime.JSX.Element;
19
+ declare function useAuth(): AuthContextValue;
20
+
21
+ export { AuthProvider, useAuth };
@@ -0,0 +1,113 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import {
3
+ createContext,
4
+ useContext,
5
+ useState,
6
+ useEffect,
7
+ useCallback
8
+ } from "react";
9
+ import { apiClient, ApiError } from '../api/client.js';
10
+ import { authApi, teamsApi } from '../api/core/index.js';
11
+ const AuthContext = createContext(void 0);
12
+ function AuthProvider({ children }) {
13
+ const [user, setUser] = useState(null);
14
+ const [team, setTeam] = useState(null);
15
+ const [teams, setTeams] = useState([]);
16
+ const [isLoading, setIsLoading] = useState(true);
17
+ useEffect(() => {
18
+ const initAuth = async () => {
19
+ try {
20
+ await apiClient.init();
21
+ const hasToken = apiClient.getToken();
22
+ const storedUser = apiClient.getStoredUser();
23
+ if (hasToken || storedUser) {
24
+ const sessionResponse = await authApi.getSession();
25
+ if (sessionResponse?.user) {
26
+ setUser(sessionResponse.user);
27
+ } else if (storedUser) {
28
+ setUser(storedUser);
29
+ } else {
30
+ await apiClient.clearAuth();
31
+ return;
32
+ }
33
+ const teamsResponse = await teamsApi.getTeams();
34
+ setTeams(teamsResponse.data);
35
+ if (teamsResponse.data.length > 0) {
36
+ const storedTeamId = apiClient.getTeamId();
37
+ const storedTeam = teamsResponse.data.find((t) => t.id === storedTeamId);
38
+ if (storedTeam) {
39
+ setTeam(storedTeam);
40
+ } else {
41
+ const firstTeam = teamsResponse.data[0];
42
+ await teamsApi.switchTeam(firstTeam.id);
43
+ setTeam(firstTeam);
44
+ }
45
+ }
46
+ }
47
+ } catch (error) {
48
+ if (error instanceof ApiError && error.status === 401) {
49
+ await apiClient.clearAuth();
50
+ } else {
51
+ console.warn("[AuthProvider] Init failed (network or server error):", error);
52
+ const storedUser = apiClient.getStoredUser();
53
+ if (storedUser) {
54
+ setUser(storedUser);
55
+ }
56
+ }
57
+ } finally {
58
+ setIsLoading(false);
59
+ }
60
+ };
61
+ initAuth();
62
+ }, []);
63
+ const login = useCallback(async (email, password) => {
64
+ setIsLoading(true);
65
+ try {
66
+ const loginResponse = await authApi.login(email, password);
67
+ setUser(loginResponse.user);
68
+ const teamsResponse = await teamsApi.getTeams();
69
+ setTeams(teamsResponse.data);
70
+ if (teamsResponse.data.length === 0) {
71
+ throw new ApiError("No teams available", 400);
72
+ }
73
+ const firstTeam = teamsResponse.data[0];
74
+ await teamsApi.switchTeam(firstTeam.id);
75
+ setTeam(firstTeam);
76
+ } finally {
77
+ setIsLoading(false);
78
+ }
79
+ }, []);
80
+ const logout = useCallback(async () => {
81
+ await authApi.logout();
82
+ setUser(null);
83
+ setTeam(null);
84
+ setTeams([]);
85
+ }, []);
86
+ const selectTeam = useCallback(async (newTeam) => {
87
+ await teamsApi.switchTeam(newTeam.id);
88
+ setTeam(newTeam);
89
+ }, []);
90
+ const value = {
91
+ user,
92
+ team,
93
+ teams,
94
+ isLoading,
95
+ isAuthenticated: !!user && !!team,
96
+ login,
97
+ logout,
98
+ selectTeam
99
+ };
100
+ return /* @__PURE__ */ jsx(AuthContext.Provider, { value, children });
101
+ }
102
+ function useAuth() {
103
+ const context = useContext(AuthContext);
104
+ if (context === void 0) {
105
+ throw new Error("useAuth must be used within an AuthProvider");
106
+ }
107
+ return context;
108
+ }
109
+ export {
110
+ AuthProvider,
111
+ useAuth
112
+ };
113
+ //# sourceMappingURL=AuthProvider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/providers/AuthProvider.tsx"],"sourcesContent":["/**\n * Authentication Provider\n */\n\nimport {\n createContext,\n useContext,\n useState,\n useEffect,\n useCallback,\n type ReactNode,\n} from 'react'\nimport { apiClient, ApiError } from '../api/client'\nimport { authApi, teamsApi } from '../api/core'\nimport type { User, Team } from '../api/core/types'\n\ninterface AuthContextValue {\n user: User | null\n team: Team | null\n teams: Team[]\n isLoading: boolean\n isAuthenticated: boolean\n login: (email: string, password: string) => Promise<void>\n logout: () => Promise<void>\n selectTeam: (team: Team) => Promise<void>\n}\n\nconst AuthContext = createContext<AuthContextValue | undefined>(undefined)\n\ninterface AuthProviderProps {\n children: ReactNode\n}\n\nexport function AuthProvider({ children }: AuthProviderProps) {\n const [user, setUser] = useState<User | null>(null)\n const [team, setTeam] = useState<Team | null>(null)\n const [teams, setTeams] = useState<Team[]>([])\n const [isLoading, setIsLoading] = useState(true)\n\n // Initialize auth state from storage\n useEffect(() => {\n const initAuth = async () => {\n try {\n await apiClient.init()\n\n // Check if we have stored credentials\n const hasToken = apiClient.getToken()\n const storedUser = apiClient.getStoredUser()\n\n if (hasToken || storedUser) {\n // Try to validate session with server and get fresh user data\n const sessionResponse = await authApi.getSession()\n\n if (sessionResponse?.user) {\n // Session is valid, use fresh user data\n setUser(sessionResponse.user)\n } else if (storedUser) {\n // Session call failed but we have stored user - try to use it\n // This allows offline-first behavior\n setUser(storedUser)\n } else {\n // No valid session and no stored user - clear auth\n await apiClient.clearAuth()\n return\n }\n\n // Get teams and restore team selection\n const teamsResponse = await teamsApi.getTeams()\n setTeams(teamsResponse.data)\n\n if (teamsResponse.data.length > 0) {\n // Check if we have a stored team ID\n const storedTeamId = apiClient.getTeamId()\n const storedTeam = teamsResponse.data.find(t => t.id === storedTeamId)\n\n if (storedTeam) {\n setTeam(storedTeam)\n } else {\n // Select first team by default\n const firstTeam = teamsResponse.data[0]\n await teamsApi.switchTeam(firstTeam.id)\n setTeam(firstTeam)\n }\n }\n }\n } catch (error) {\n // Only clear auth for authentication failures (401)\n // For network errors or other issues, keep credentials to allow retry\n if (error instanceof ApiError && error.status === 401) {\n await apiClient.clearAuth()\n } else {\n console.warn('[AuthProvider] Init failed (network or server error):', error)\n // Try to use stored user for offline-first behavior\n const storedUser = apiClient.getStoredUser()\n if (storedUser) {\n setUser(storedUser)\n }\n }\n } finally {\n setIsLoading(false)\n }\n }\n\n initAuth()\n }, [])\n\n const login = useCallback(async (email: string, password: string) => {\n setIsLoading(true)\n\n try {\n // Login to get token\n const loginResponse = await authApi.login(email, password)\n setUser(loginResponse.user)\n\n // Get user's teams\n const teamsResponse = await teamsApi.getTeams()\n setTeams(teamsResponse.data)\n\n if (teamsResponse.data.length === 0) {\n throw new ApiError('No teams available', 400)\n }\n\n // Select first team\n const firstTeam = teamsResponse.data[0]\n await teamsApi.switchTeam(firstTeam.id)\n setTeam(firstTeam)\n } finally {\n setIsLoading(false)\n }\n }, [])\n\n const logout = useCallback(async () => {\n await authApi.logout()\n setUser(null)\n setTeam(null)\n setTeams([])\n }, [])\n\n const selectTeam = useCallback(async (newTeam: Team) => {\n await teamsApi.switchTeam(newTeam.id)\n setTeam(newTeam)\n }, [])\n\n const value: AuthContextValue = {\n user,\n team,\n teams,\n isLoading,\n isAuthenticated: !!user && !!team,\n login,\n logout,\n selectTeam,\n }\n\n return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>\n}\n\nexport function useAuth() {\n const context = useContext(AuthContext)\n\n if (context === undefined) {\n throw new Error('useAuth must be used within an AuthProvider')\n }\n\n return context\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,11 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { QueryClient } from '@tanstack/react-query';
3
+ import { ReactNode } from 'react';
4
+
5
+ declare const queryClient: QueryClient;
6
+ interface QueryProviderProps {
7
+ children: ReactNode;
8
+ }
9
+ declare function QueryProvider({ children }: QueryProviderProps): react_jsx_runtime.JSX.Element;
10
+
11
+ export { QueryProvider, queryClient };
@@ -0,0 +1,23 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
3
+ const queryClient = new QueryClient({
4
+ defaultOptions: {
5
+ queries: {
6
+ staleTime: 1e3 * 60 * 5,
7
+ // 5 minutes
8
+ retry: 1,
9
+ refetchOnWindowFocus: false
10
+ },
11
+ mutations: {
12
+ retry: 0
13
+ }
14
+ }
15
+ });
16
+ function QueryProvider({ children }) {
17
+ return /* @__PURE__ */ jsx(QueryClientProvider, { client: queryClient, children });
18
+ }
19
+ export {
20
+ QueryProvider,
21
+ queryClient
22
+ };
23
+ //# sourceMappingURL=QueryProvider.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/providers/QueryProvider.tsx"],"sourcesContent":["/**\n * TanStack Query Provider\n */\n\nimport { QueryClient, QueryClientProvider } from '@tanstack/react-query'\nimport type { ReactNode } from 'react'\n\nconst queryClient = new QueryClient({\n defaultOptions: {\n queries: {\n staleTime: 1000 * 60 * 5, // 5 minutes\n retry: 1,\n refetchOnWindowFocus: false,\n },\n mutations: {\n retry: 0,\n },\n },\n})\n\ninterface QueryProviderProps {\n children: ReactNode\n}\n\nexport function QueryProvider({ children }: QueryProviderProps) {\n return (\n <QueryClientProvider client={queryClient}>\n {children}\n </QueryClientProvider>\n )\n}\n\nexport { queryClient }\n"],"mappings":"AA0BI;AAtBJ,SAAS,aAAa,2BAA2B;AAGjD,MAAM,cAAc,IAAI,YAAY;AAAA,EAClC,gBAAgB;AAAA,IACd,SAAS;AAAA,MACP,WAAW,MAAO,KAAK;AAAA;AAAA,MACvB,OAAO;AAAA,MACP,sBAAsB;AAAA,IACxB;AAAA,IACA,WAAW;AAAA,MACT,OAAO;AAAA,IACT;AAAA,EACF;AACF,CAAC;AAMM,SAAS,cAAc,EAAE,SAAS,GAAuB;AAC9D,SACE,oBAAC,uBAAoB,QAAQ,aAC1B,UACH;AAEJ;","names":[]}
@@ -0,0 +1,6 @@
1
+ export { AuthProvider, useAuth } from './AuthProvider.js';
2
+ export { QueryProvider, queryClient } from './QueryProvider.js';
3
+ import 'react/jsx-runtime';
4
+ import 'react';
5
+ import '../api/core/types.js';
6
+ import '@tanstack/react-query';
@@ -0,0 +1,9 @@
1
+ import { AuthProvider, useAuth } from './AuthProvider.js';
2
+ import { QueryProvider, queryClient } from './QueryProvider.js';
3
+ export {
4
+ AuthProvider,
5
+ QueryProvider,
6
+ queryClient,
7
+ useAuth
8
+ };
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/providers/index.ts"],"sourcesContent":["/**\n * Providers\n */\n\nexport { AuthProvider, useAuth } from './AuthProvider'\nexport { QueryProvider, queryClient } from './QueryProvider'\n"],"mappings":"AAIA,SAAS,cAAc,eAAe;AACtC,SAAS,eAAe,mBAAmB;","names":[]}
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Cross-platform secure storage
3
+ * Uses expo-secure-store on native, localStorage on web
4
+ *
5
+ * ⚠️ SECURITY NOTE: Web storage uses localStorage which is vulnerable to XSS attacks.
6
+ * For production web deployments, consider:
7
+ * - Using httpOnly cookies for session management (requires backend changes)
8
+ * - Implementing Content Security Policy (CSP)
9
+ * - This is acceptable for development/testing and native mobile apps
10
+ */
11
+ declare function getItemAsync(key: string): Promise<string | null>;
12
+ declare function setItemAsync(key: string, value: string): Promise<void>;
13
+ declare function deleteItemAsync(key: string): Promise<void>;
14
+
15
+ declare const storage_deleteItemAsync: typeof deleteItemAsync;
16
+ declare const storage_getItemAsync: typeof getItemAsync;
17
+ declare const storage_setItemAsync: typeof setItemAsync;
18
+ declare namespace storage {
19
+ export { storage_deleteItemAsync as deleteItemAsync, storage_getItemAsync as getItemAsync, storage_setItemAsync as setItemAsync };
20
+ }
21
+
22
+ export { setItemAsync as a, deleteItemAsync as d, getItemAsync as g, storage as s };
package/package.json ADDED
@@ -0,0 +1,99 @@
1
+ {
2
+ "name": "@nextsparkjs/mobile",
3
+ "version": "0.1.0-beta.1",
4
+ "description": "Mobile app infrastructure for NextSpark - API client, providers, and utilities for Expo apps",
5
+ "license": "MIT",
6
+ "author": "NextSpark <hello@nextspark.dev>",
7
+ "homepage": "https://nextspark.dev",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/NextSpark-js/nextspark.git",
11
+ "directory": "packages/mobile"
12
+ },
13
+ "type": "module",
14
+ "main": "./dist/index.js",
15
+ "module": "./dist/index.js",
16
+ "types": "./dist/index.d.ts",
17
+ "exports": {
18
+ ".": {
19
+ "types": "./dist/index.d.ts",
20
+ "import": "./dist/index.js"
21
+ },
22
+ "./api": {
23
+ "types": "./dist/api/index.d.ts",
24
+ "import": "./dist/api/index.js"
25
+ },
26
+ "./api/core": {
27
+ "types": "./dist/api/core/index.d.ts",
28
+ "import": "./dist/api/core/index.js"
29
+ },
30
+ "./api/entities": {
31
+ "types": "./dist/api/entities/index.d.ts",
32
+ "import": "./dist/api/entities/index.js"
33
+ },
34
+ "./providers": {
35
+ "types": "./dist/providers/index.d.ts",
36
+ "import": "./dist/providers/index.js"
37
+ },
38
+ "./hooks": {
39
+ "types": "./dist/hooks/index.d.ts",
40
+ "import": "./dist/hooks/index.js"
41
+ },
42
+ "./lib": {
43
+ "types": "./dist/lib/index.d.ts",
44
+ "import": "./dist/lib/index.js"
45
+ }
46
+ },
47
+ "files": [
48
+ "dist",
49
+ "templates",
50
+ "README.md"
51
+ ],
52
+ "scripts": {
53
+ "build": "tsup",
54
+ "build:watch": "tsup --watch",
55
+ "test": "jest",
56
+ "test:watch": "jest --watch",
57
+ "test:coverage": "jest --coverage",
58
+ "typecheck": "tsc --noEmit",
59
+ "clean": "rm -rf dist"
60
+ },
61
+ "peerDependencies": {
62
+ "@tanstack/react-query": "^5.0.0",
63
+ "expo": ">=54.0.0",
64
+ "expo-constants": ">=18.0.0",
65
+ "expo-secure-store": ">=15.0.0",
66
+ "react": ">=18.0.0",
67
+ "react-native": ">=0.75.0"
68
+ },
69
+ "devDependencies": {
70
+ "@babel/core": "^7.25.0",
71
+ "@babel/preset-env": "^7.25.0",
72
+ "@babel/preset-react": "^7.25.0",
73
+ "@babel/preset-typescript": "^7.25.0",
74
+ "@testing-library/react-native": "^13.0.0",
75
+ "@types/jest": "^29.5.0",
76
+ "@types/node": "^22.0.0",
77
+ "@types/react": "^19",
78
+ "babel-jest": "^29.7.0",
79
+ "glob": "^11.0.0",
80
+ "jest": "^29.7.0",
81
+ "jest-expo": "^54.0.0",
82
+ "react": "19.1.0",
83
+ "react-native": "0.81.5",
84
+ "react-test-renderer": "19.1.0",
85
+ "tsup": "^8.5.0",
86
+ "typescript": "^5"
87
+ },
88
+ "keywords": [
89
+ "nextspark",
90
+ "mobile",
91
+ "expo",
92
+ "react-native",
93
+ "api-client",
94
+ "authentication"
95
+ ],
96
+ "nextspark": {
97
+ "type": "mobile"
98
+ }
99
+ }
@@ -0,0 +1,216 @@
1
+ /**
2
+ * App Layout - NextSpark Mobile Style with Bottom Tab Navigation
3
+ */
4
+
5
+ import { useEffect, useState, useMemo } from 'react'
6
+ import { View, StyleSheet } from 'react-native'
7
+ import { Alert, useAuth } from '@nextsparkjs/mobile'
8
+ import { Stack, router } from 'expo-router'
9
+ import { useQueryClient } from '@tanstack/react-query'
10
+ import {
11
+ TopBar,
12
+ BottomTabBar,
13
+ MoreSheet,
14
+ CreateSheet,
15
+ type TabKey,
16
+ } from '@/src/components/navigation'
17
+ import { Colors } from '@/src/constants/colors'
18
+ import notificationsData from '@/src/data/notifications.mock.json'
19
+
20
+ export default function AppLayout() {
21
+ const { isAuthenticated, isLoading, logout } = useAuth()
22
+ const queryClient = useQueryClient()
23
+ const [activeTab, setActiveTab] = useState<TabKey>('home')
24
+ const [moreSheetVisible, setMoreSheetVisible] = useState(false)
25
+ const [createSheetVisible, setCreateSheetVisible] = useState(false)
26
+
27
+ // Calculate unread notifications count from mock data
28
+ const unreadNotificationCount = useMemo(
29
+ () => notificationsData.notifications.filter((n) => !n.read).length,
30
+ []
31
+ )
32
+
33
+ useEffect(() => {
34
+ if (!isLoading && !isAuthenticated) {
35
+ router.replace('/login')
36
+ }
37
+ }, [isAuthenticated, isLoading])
38
+
39
+ if (isLoading || !isAuthenticated) {
40
+ return null
41
+ }
42
+
43
+ const handleTabPress = (tab: TabKey) => {
44
+ if (tab === 'more') {
45
+ setMoreSheetVisible(true)
46
+ return
47
+ }
48
+
49
+ if (tab === 'create') {
50
+ setCreateSheetVisible(true)
51
+ return
52
+ }
53
+
54
+ setActiveTab(tab)
55
+
56
+ // Navigate based on tab
57
+ switch (tab) {
58
+ case 'home':
59
+ router.replace('/(app)' as const)
60
+ break
61
+ case 'tasks':
62
+ router.replace('/(app)/tasks')
63
+ break
64
+ case 'customers':
65
+ router.replace('/(app)/customers')
66
+ break
67
+ }
68
+ }
69
+
70
+ const handleMoreNavigate = (screen: string) => {
71
+ switch (screen) {
72
+ case 'profile':
73
+ router.push('/(app)/profile')
74
+ break
75
+ case 'settings':
76
+ router.push('/(app)/settings')
77
+ break
78
+ case 'billing':
79
+ // TODO: Add billing screen
80
+ break
81
+ case 'api-keys':
82
+ // TODO: Add API keys screen
83
+ break
84
+ }
85
+ }
86
+
87
+ const handleCreateEntity = (entity: string) => {
88
+ switch (entity) {
89
+ case 'customer':
90
+ router.push('/(app)/customer/create')
91
+ break
92
+ case 'task':
93
+ router.push('/(app)/task/create')
94
+ break
95
+ }
96
+ }
97
+
98
+ const handleLogout = async () => {
99
+ setMoreSheetVisible(false)
100
+ const confirmed = await Alert.confirmDestructive(
101
+ 'Cerrar Sesión',
102
+ '¿Estás seguro que deseas salir?',
103
+ 'Salir'
104
+ )
105
+
106
+ if (confirmed) {
107
+ await logout()
108
+ router.replace('/login')
109
+ }
110
+ }
111
+
112
+ const handleTeamChange = () => {
113
+ // Invalidate all queries to refresh data for the new team
114
+ queryClient.invalidateQueries()
115
+ // Navigate to home to show refreshed data
116
+ setActiveTab('home')
117
+ router.replace('/(app)' as const)
118
+ }
119
+
120
+ return (
121
+ <View style={styles.container}>
122
+ {/* Top Bar */}
123
+ <TopBar notificationCount={unreadNotificationCount} />
124
+
125
+ {/* Screen Content */}
126
+ <View style={styles.content}>
127
+ <Stack
128
+ screenOptions={{
129
+ headerShown: false,
130
+ contentStyle: { backgroundColor: Colors.backgroundSecondary },
131
+ }}
132
+ >
133
+ <Stack.Screen name="index" />
134
+ <Stack.Screen name="tasks" />
135
+ <Stack.Screen name="customers" />
136
+ <Stack.Screen name="settings" />
137
+ <Stack.Screen name="profile" />
138
+ <Stack.Screen
139
+ name="notifications"
140
+ options={{
141
+ headerShown: false,
142
+ presentation: 'modal',
143
+ }}
144
+ />
145
+ <Stack.Screen
146
+ name="task/create"
147
+ options={{
148
+ presentation: 'modal',
149
+ headerShown: true,
150
+ headerTitle: 'Nueva Tarea',
151
+ headerStyle: { backgroundColor: Colors.background },
152
+ headerTintColor: Colors.foreground,
153
+ }}
154
+ />
155
+ <Stack.Screen
156
+ name="task/[id]"
157
+ options={{
158
+ headerShown: true,
159
+ headerTitle: 'Editar Tarea',
160
+ headerStyle: { backgroundColor: Colors.background },
161
+ headerTintColor: Colors.foreground,
162
+ }}
163
+ />
164
+ <Stack.Screen
165
+ name="customer/create"
166
+ options={{
167
+ presentation: 'modal',
168
+ headerShown: true,
169
+ headerTitle: 'Nuevo Cliente',
170
+ headerStyle: { backgroundColor: Colors.background },
171
+ headerTintColor: Colors.foreground,
172
+ }}
173
+ />
174
+ <Stack.Screen
175
+ name="customer/[id]"
176
+ options={{
177
+ headerShown: true,
178
+ headerTitle: 'Editar Cliente',
179
+ headerStyle: { backgroundColor: Colors.background },
180
+ headerTintColor: Colors.foreground,
181
+ }}
182
+ />
183
+ </Stack>
184
+ </View>
185
+
186
+ {/* Bottom Tab Bar */}
187
+ <BottomTabBar activeTab={activeTab} onTabPress={handleTabPress} />
188
+
189
+ {/* More Options Sheet */}
190
+ <MoreSheet
191
+ visible={moreSheetVisible}
192
+ onClose={() => setMoreSheetVisible(false)}
193
+ onNavigate={handleMoreNavigate}
194
+ onLogout={handleLogout}
195
+ onTeamChange={handleTeamChange}
196
+ />
197
+
198
+ {/* Create Sheet */}
199
+ <CreateSheet
200
+ visible={createSheetVisible}
201
+ onClose={() => setCreateSheetVisible(false)}
202
+ onCreateEntity={handleCreateEntity}
203
+ />
204
+ </View>
205
+ )
206
+ }
207
+
208
+ const styles = StyleSheet.create({
209
+ container: {
210
+ flex: 1,
211
+ backgroundColor: Colors.background,
212
+ },
213
+ content: {
214
+ flex: 1,
215
+ },
216
+ })