create-stackr 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/LICENSE +21 -0
- package/README.md +642 -0
- package/bin/cli.js +12 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +113 -0
- package/dist/cli.js.map +1 -0
- package/dist/config/dependencies.d.ts +82 -0
- package/dist/config/dependencies.d.ts.map +1 -0
- package/dist/config/dependencies.js +82 -0
- package/dist/config/dependencies.js.map +1 -0
- package/dist/config/presets.d.ts +3 -0
- package/dist/config/presets.d.ts.map +1 -0
- package/dist/config/presets.js +174 -0
- package/dist/config/presets.js.map +1 -0
- package/dist/generators/index.d.ts +40 -0
- package/dist/generators/index.d.ts.map +1 -0
- package/dist/generators/index.js +130 -0
- package/dist/generators/index.js.map +1 -0
- package/dist/generators/onboarding.d.ts +8 -0
- package/dist/generators/onboarding.d.ts.map +1 -0
- package/dist/generators/onboarding.js +141 -0
- package/dist/generators/onboarding.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +65 -0
- package/dist/index.js.map +1 -0
- package/dist/prompts/features.d.ts +14 -0
- package/dist/prompts/features.d.ts.map +1 -0
- package/dist/prompts/features.js +96 -0
- package/dist/prompts/features.js.map +1 -0
- package/dist/prompts/index.d.ts +3 -0
- package/dist/prompts/index.d.ts.map +1 -0
- package/dist/prompts/index.js +93 -0
- package/dist/prompts/index.js.map +1 -0
- package/dist/prompts/onboarding.d.ts +6 -0
- package/dist/prompts/onboarding.d.ts.map +1 -0
- package/dist/prompts/onboarding.js +37 -0
- package/dist/prompts/onboarding.js.map +1 -0
- package/dist/prompts/orm.d.ts +3 -0
- package/dist/prompts/orm.d.ts.map +1 -0
- package/dist/prompts/orm.js +23 -0
- package/dist/prompts/orm.js.map +1 -0
- package/dist/prompts/packageManager.d.ts +2 -0
- package/dist/prompts/packageManager.d.ts.map +1 -0
- package/dist/prompts/packageManager.js +18 -0
- package/dist/prompts/packageManager.js.map +1 -0
- package/dist/prompts/platform.d.ts +3 -0
- package/dist/prompts/platform.d.ts.map +1 -0
- package/dist/prompts/platform.js +21 -0
- package/dist/prompts/platform.js.map +1 -0
- package/dist/prompts/preset.d.ts +4 -0
- package/dist/prompts/preset.d.ts.map +1 -0
- package/dist/prompts/preset.js +165 -0
- package/dist/prompts/preset.js.map +1 -0
- package/dist/prompts/project.d.ts +2 -0
- package/dist/prompts/project.d.ts.map +1 -0
- package/dist/prompts/project.js +27 -0
- package/dist/prompts/project.js.map +1 -0
- package/dist/prompts/sdks.d.ts +2 -0
- package/dist/prompts/sdks.d.ts.map +1 -0
- package/dist/prompts/sdks.js +46 -0
- package/dist/prompts/sdks.js.map +1 -0
- package/dist/types/index.d.ts +77 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +25 -0
- package/dist/types/index.js.map +1 -0
- package/dist/utils/cleanup.d.ts +5 -0
- package/dist/utils/cleanup.d.ts.map +1 -0
- package/dist/utils/cleanup.js +38 -0
- package/dist/utils/cleanup.js.map +1 -0
- package/dist/utils/copy.d.ts +10 -0
- package/dist/utils/copy.d.ts.map +1 -0
- package/dist/utils/copy.js +53 -0
- package/dist/utils/copy.js.map +1 -0
- package/dist/utils/errors.d.ts +33 -0
- package/dist/utils/errors.d.ts.map +1 -0
- package/dist/utils/errors.js +136 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/git.d.ts +5 -0
- package/dist/utils/git.d.ts.map +1 -0
- package/dist/utils/git.js +33 -0
- package/dist/utils/git.js.map +1 -0
- package/dist/utils/logger.d.ts +9 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +22 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/package.d.ts +16 -0
- package/dist/utils/package.d.ts.map +1 -0
- package/dist/utils/package.js +86 -0
- package/dist/utils/package.js.map +1 -0
- package/dist/utils/system-validation.d.ts +9 -0
- package/dist/utils/system-validation.d.ts.map +1 -0
- package/dist/utils/system-validation.js +31 -0
- package/dist/utils/system-validation.js.map +1 -0
- package/dist/utils/template.d.ts +20 -0
- package/dist/utils/template.d.ts.map +1 -0
- package/dist/utils/template.js +234 -0
- package/dist/utils/template.js.map +1 -0
- package/dist/utils/validation.d.ts +8 -0
- package/dist/utils/validation.d.ts.map +1 -0
- package/dist/utils/validation.js +94 -0
- package/dist/utils/validation.js.map +1 -0
- package/package.json +96 -0
- package/templates/base/backend/.dockerignore.ejs +62 -0
- package/templates/base/backend/.env.example.ejs +116 -0
- package/templates/base/backend/Dockerfile.ejs +142 -0
- package/templates/base/backend/controllers/event-queue/index.ts +20 -0
- package/templates/base/backend/controllers/event-queue/workers/user.ts +39 -0
- package/templates/base/backend/controllers/rest-api/index.ts +48 -0
- package/templates/base/backend/controllers/rest-api/plugins/auth.ts +152 -0
- package/templates/base/backend/controllers/rest-api/plugins/config.ts +64 -0
- package/templates/base/backend/controllers/rest-api/plugins/error-handler.ts +118 -0
- package/templates/base/backend/controllers/rest-api/routes/auth.ts.ejs +180 -0
- package/templates/base/backend/controllers/rest-api/routes/device-sessions.ts +197 -0
- package/templates/base/backend/controllers/rest-api/routes/oauth-web.ts.ejs +375 -0
- package/templates/base/backend/controllers/rest-api/server.ts.ejs +87 -0
- package/templates/base/backend/domain/device-session/repository.drizzle.ts +209 -0
- package/templates/base/backend/domain/device-session/repository.prisma.ts +248 -0
- package/templates/base/backend/domain/device-session/schema.ts +72 -0
- package/templates/base/backend/domain/session/repository.drizzle.ts +72 -0
- package/templates/base/backend/domain/session/repository.prisma.ts +72 -0
- package/templates/base/backend/domain/session/schema.ts +29 -0
- package/templates/base/backend/domain/user/repository.drizzle.ts +127 -0
- package/templates/base/backend/domain/user/repository.prisma.ts +115 -0
- package/templates/base/backend/domain/user/schema.ts +14 -0
- package/templates/base/backend/drizzle/schema.drizzle.ts +111 -0
- package/templates/base/backend/drizzle.config.drizzle.ts +13 -0
- package/templates/base/backend/lib/auth.drizzle.ts.ejs +104 -0
- package/templates/base/backend/lib/auth.prisma.ts.ejs +97 -0
- package/templates/base/backend/lib/constants.ts.ejs +29 -0
- package/templates/base/backend/package.json.ejs +50 -0
- package/templates/base/backend/prisma/schema.prisma.ejs +102 -0
- package/templates/base/backend/prisma.config.prisma.ts +12 -0
- package/templates/base/backend/tsconfig.json +39 -0
- package/templates/base/backend/utils/db.drizzle.ts +41 -0
- package/templates/base/backend/utils/db.prisma.ts +51 -0
- package/templates/base/backend/utils/email.ts.ejs +35 -0
- package/templates/base/backend/utils/errors.ts +348 -0
- package/templates/base/backend/utils/redis.ts.ejs +279 -0
- package/templates/base/mobile/.env.example.ejs +35 -0
- package/templates/base/mobile/.gitignore.ejs +167 -0
- package/templates/base/mobile/app/+not-found.tsx +85 -0
- package/templates/base/mobile/app/_layout.tsx.ejs +71 -0
- package/templates/base/mobile/app.json.ejs +88 -0
- package/templates/base/mobile/assets/images/adaptive-icon.png +0 -0
- package/templates/base/mobile/assets/images/favicon.png +0 -0
- package/templates/base/mobile/assets/images/icon.png +0 -0
- package/templates/base/mobile/assets/images/onboarding_page_1.png +0 -0
- package/templates/base/mobile/assets/images/onboarding_page_2.png +0 -0
- package/templates/base/mobile/assets/images/onboarding_page_3.png +0 -0
- package/templates/base/mobile/assets/images/paywall_image.png +0 -0
- package/templates/base/mobile/assets/images/splash.png +0 -0
- package/templates/base/mobile/eas.json.ejs +49 -0
- package/templates/base/mobile/metro.config.js +9 -0
- package/templates/base/mobile/package.json.ejs +53 -0
- package/templates/base/mobile/src/components/ui/Button.tsx +131 -0
- package/templates/base/mobile/src/components/ui/Card.tsx +68 -0
- package/templates/base/mobile/src/components/ui/IconSymbol.tsx +90 -0
- package/templates/base/mobile/src/components/ui/Input.tsx +142 -0
- package/templates/base/mobile/src/components/ui/LoadingSpinner.tsx +98 -0
- package/templates/base/mobile/src/components/ui/OnboardingLayout.tsx +356 -0
- package/templates/base/mobile/src/components/ui/PaywallLayout.tsx +311 -0
- package/templates/base/mobile/src/components/ui/Skeleton.tsx +58 -0
- package/templates/base/mobile/src/components/ui/index.ts +6 -0
- package/templates/base/mobile/src/constants/Theme.ts +163 -0
- package/templates/base/mobile/src/context/ThemeContext.tsx +157 -0
- package/templates/base/mobile/src/lib/auth-client.ts.ejs +51 -0
- package/templates/base/mobile/src/services/api.ts.ejs +71 -0
- package/templates/base/mobile/src/services/errorService.ts +179 -0
- package/templates/base/mobile/src/services/sdkInitializer.ts.ejs +36 -0
- package/templates/base/mobile/src/store/index.ts.ejs +18 -0
- package/templates/base/mobile/src/store/ui.store.ts +100 -0
- package/templates/base/mobile/src/utils/formatters.ts +105 -0
- package/templates/base/mobile/src/utils/logger.ts +73 -0
- package/templates/base/mobile/src/utils/responsive.ts +234 -0
- package/templates/base/mobile/tsconfig.json +32 -0
- package/templates/base/web/.env.example.ejs +26 -0
- package/templates/base/web/components.json +22 -0
- package/templates/base/web/eslint.config.mjs +18 -0
- package/templates/base/web/next.config.ts +7 -0
- package/templates/base/web/package.json.ejs +35 -0
- package/templates/base/web/postcss.config.mjs +7 -0
- package/templates/base/web/public/.gitkeep +0 -0
- package/templates/base/web/public/file.svg +1 -0
- package/templates/base/web/public/globe.svg +1 -0
- package/templates/base/web/public/next.svg +1 -0
- package/templates/base/web/public/vercel.svg +1 -0
- package/templates/base/web/public/window.svg +1 -0
- package/templates/base/web/src/app/favicon.ico +0 -0
- package/templates/base/web/src/app/globals.css +152 -0
- package/templates/base/web/src/app/layout.tsx.ejs +54 -0
- package/templates/base/web/src/app/page.tsx.ejs +92 -0
- package/templates/base/web/src/components/auth/auth-hydrator.tsx.ejs +19 -0
- package/templates/base/web/src/components/auth/protected-route.tsx.ejs +109 -0
- package/templates/base/web/src/components/providers/device-session-setup.tsx.ejs +56 -0
- package/templates/base/web/src/components/providers/theme-provider.tsx +17 -0
- package/templates/base/web/src/components/theme-toggle.tsx +34 -0
- package/templates/base/web/src/components/ui/button.tsx +62 -0
- package/templates/base/web/src/components/ui/card.tsx +92 -0
- package/templates/base/web/src/components/ui/input.tsx +21 -0
- package/templates/base/web/src/components/ui/label.tsx +24 -0
- package/templates/base/web/src/components/ui/skeleton.tsx +13 -0
- package/templates/base/web/src/components/ui/spinner.tsx +20 -0
- package/templates/base/web/src/hooks/use-device-session.ts.ejs +40 -0
- package/templates/base/web/src/hooks/use-session.ts.ejs +56 -0
- package/templates/base/web/src/lib/auth/actions.ts.ejs +334 -0
- package/templates/base/web/src/lib/auth/config.ts.ejs +65 -0
- package/templates/base/web/src/lib/auth/cookies.ts.ejs +74 -0
- package/templates/base/web/src/lib/auth/index.ts.ejs +40 -0
- package/templates/base/web/src/lib/auth/oauth.ts.ejs +72 -0
- package/templates/base/web/src/lib/auth/pkce.ts.ejs +48 -0
- package/templates/base/web/src/lib/auth/sessions.ts.ejs +135 -0
- package/templates/base/web/src/lib/auth/user-agent.ts.ejs +47 -0
- package/templates/base/web/src/lib/device/actions.ts.ejs +148 -0
- package/templates/base/web/src/lib/device/id.ts.ejs +74 -0
- package/templates/base/web/src/lib/utils.ts +6 -0
- package/templates/base/web/src/proxy.ts.ejs +66 -0
- package/templates/base/web/src/store/auth.store.ts.ejs +89 -0
- package/templates/base/web/src/store/deviceSession.store.ts.ejs +141 -0
- package/templates/base/web/tsconfig.json +34 -0
- package/templates/features/mobile/auth/app/(auth)/_layout.tsx +16 -0
- package/templates/features/mobile/auth/app/(auth)/login.tsx +86 -0
- package/templates/features/mobile/auth/app/(auth)/register.tsx +86 -0
- package/templates/features/mobile/auth/components/auth/LoginForm.tsx.ejs +349 -0
- package/templates/features/mobile/auth/components/auth/RegisterForm.tsx.ejs +407 -0
- package/templates/features/mobile/auth/components/auth/index.ts +2 -0
- package/templates/features/mobile/auth/hooks/index.ts.ejs +1 -0
- package/templates/features/mobile/auth/hooks/useAuth.ts.ejs +367 -0
- package/templates/features/mobile/auth/services/deviceSession.ts +370 -0
- package/templates/features/mobile/auth/store/deviceSession.store.ts +326 -0
- package/templates/features/mobile/onboarding/app/(onboarding)/_layout.tsx.ejs +11 -0
- package/templates/features/mobile/onboarding/app/(onboarding)/page-1.tsx.ejs +52 -0
- package/templates/features/mobile/onboarding/app/(onboarding)/page-2.tsx.ejs +52 -0
- package/templates/features/mobile/onboarding/app/(onboarding)/page-3.tsx.ejs +60 -0
- package/templates/features/mobile/paywall/app/paywall.tsx +550 -0
- package/templates/features/mobile/tabs/app/(tabs)/_layout.tsx +26 -0
- package/templates/features/mobile/tabs/app/(tabs)/index.tsx +565 -0
- package/templates/features/web/.gitkeep +0 -0
- package/templates/features/web/auth/app/(app)/dashboard/dashboard-client.tsx.ejs +166 -0
- package/templates/features/web/auth/app/(app)/dashboard/page.tsx.ejs +24 -0
- package/templates/features/web/auth/app/(app)/layout.tsx.ejs +43 -0
- package/templates/features/web/auth/app/(app)/settings/sessions/page.tsx.ejs +29 -0
- package/templates/features/web/auth/app/(app)/settings/sessions/sessions-client.tsx.ejs +77 -0
- package/templates/features/web/auth/app/(auth)/forgot-password/page.tsx.ejs +127 -0
- package/templates/features/web/auth/app/(auth)/layout.tsx.ejs +32 -0
- package/templates/features/web/auth/app/(auth)/login/page.tsx.ejs +35 -0
- package/templates/features/web/auth/app/(auth)/register/page.tsx.ejs +19 -0
- package/templates/features/web/auth/app/(auth)/reset-password/page.tsx.ejs +40 -0
- package/templates/features/web/auth/app/(auth)/verify-email/page.tsx.ejs +198 -0
- package/templates/features/web/auth/app/auth/callback/route.ts.ejs +152 -0
- package/templates/features/web/auth/components/auth/login-form.tsx.ejs +100 -0
- package/templates/features/web/auth/components/auth/oauth-buttons.tsx.ejs +126 -0
- package/templates/features/web/auth/components/auth/password-reset-form.tsx.ejs +103 -0
- package/templates/features/web/auth/components/auth/register-form.tsx.ejs +139 -0
- package/templates/features/web/auth/components/settings/session-card.tsx.ejs +132 -0
- package/templates/integrations/mobile/adjust/services/adjustService.ts.ejs +163 -0
- package/templates/integrations/mobile/adjust/store/adjust.store.ts +243 -0
- package/templates/integrations/mobile/att/services/attService.ts +84 -0
- package/templates/integrations/mobile/att/services/trackingPermissions.ts +208 -0
- package/templates/integrations/mobile/att/store/att.store.ts +162 -0
- package/templates/integrations/mobile/revenuecat/services/revenuecatService.ts.ejs +174 -0
- package/templates/integrations/mobile/revenuecat/store/revenuecat.store.ts +286 -0
- package/templates/integrations/mobile/scate/services/scateService.ts.ejs +85 -0
- package/templates/integrations/mobile/scate/store/scate.store.ts +125 -0
- package/templates/integrations/web/.gitkeep +0 -0
- package/templates/shared/.env.example.ejs +21 -0
- package/templates/shared/.gitignore.ejs +145 -0
- package/templates/shared/README.md.ejs +134 -0
- package/templates/shared/docker-compose.prod.yml.ejs +120 -0
- package/templates/shared/docker-compose.yml.ejs +129 -0
- package/templates/shared/scripts/docker-dev.sh.ejs +395 -0
- package/templates/shared/scripts/docker-prod.sh.ejs +542 -0
- package/templates/shared/scripts/setup.sh.ejs +979 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { createAuthClient } from "better-auth/react";
|
|
2
|
+
import { expoClient } from "@better-auth/expo/client";
|
|
3
|
+
import * as SecureStore from "expo-secure-store";
|
|
4
|
+
import Constants from "expo-constants";
|
|
5
|
+
|
|
6
|
+
// Get API URL from Expo config or default
|
|
7
|
+
const API_URL = Constants.expoConfig?.extra?.apiUrl || "http://localhost:8080";
|
|
8
|
+
|
|
9
|
+
// Get app scheme from Expo config (defined at expo.scheme in app.json)
|
|
10
|
+
const APP_SCHEME = Constants.expoConfig?.scheme || "<%= appScheme %>";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* BetterAuth client configured for Expo/React Native
|
|
14
|
+
*
|
|
15
|
+
* Features:
|
|
16
|
+
* - Automatic OAuth deep link handling (callbackURL paths become deep links)
|
|
17
|
+
* - Secure session storage via expo-secure-store
|
|
18
|
+
* - Session caching to avoid loading spinners on app reload
|
|
19
|
+
*/
|
|
20
|
+
export const authClient = createAuthClient({
|
|
21
|
+
baseURL: API_URL,
|
|
22
|
+
plugins: [
|
|
23
|
+
expoClient({
|
|
24
|
+
scheme: APP_SCHEME, // Must match app.json scheme
|
|
25
|
+
storagePrefix: APP_SCHEME, // Prefix for secure storage keys
|
|
26
|
+
storage: SecureStore, // Use secure storage (not AsyncStorage!)
|
|
27
|
+
}),
|
|
28
|
+
],
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// Export commonly used methods and hooks
|
|
32
|
+
export const {
|
|
33
|
+
signIn,
|
|
34
|
+
signUp,
|
|
35
|
+
signOut,
|
|
36
|
+
useSession,
|
|
37
|
+
getSession,
|
|
38
|
+
} = authClient;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Get cookies for authenticated API requests
|
|
42
|
+
* Use this when making custom API calls that need authentication
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* const cookies = getCookies();
|
|
46
|
+
* const response = await fetch(url, {
|
|
47
|
+
* headers: { "Cookie": cookies || "" },
|
|
48
|
+
* credentials: "omit"
|
|
49
|
+
* });
|
|
50
|
+
*/
|
|
51
|
+
export const getCookies = () => authClient.getCookie();
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
3
|
+
import Constants from 'expo-constants';
|
|
4
|
+
<% if (features.authentication.enabled) { %>
|
|
5
|
+
import { authClient } from '../lib/auth-client';
|
|
6
|
+
<% } %>
|
|
7
|
+
|
|
8
|
+
// Storage key for device sessions (anonymous users)
|
|
9
|
+
const DEVICE_SESSION_TOKEN_KEY = 'device_session_token';
|
|
10
|
+
|
|
11
|
+
// API Configuration
|
|
12
|
+
const API_URL = Constants.expoConfig?.extra?.apiUrl || 'http://localhost:8080';
|
|
13
|
+
|
|
14
|
+
const api = axios.create({
|
|
15
|
+
baseURL: API_URL,
|
|
16
|
+
timeout: 10000, // 10 second timeout
|
|
17
|
+
headers: {
|
|
18
|
+
'Content-Type': 'application/json',
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// Request interceptor to add auth cookies and device session token
|
|
23
|
+
api.interceptors.request.use(
|
|
24
|
+
async (config) => {
|
|
25
|
+
try {
|
|
26
|
+
<% if (features.authentication.enabled) { %>
|
|
27
|
+
// Add BetterAuth session cookies for authenticated requests
|
|
28
|
+
const cookies = authClient.getCookie();
|
|
29
|
+
if (cookies) {
|
|
30
|
+
config.headers.Cookie = cookies;
|
|
31
|
+
}
|
|
32
|
+
<% } %>
|
|
33
|
+
|
|
34
|
+
// Add device session token if available (for anonymous sessions)
|
|
35
|
+
const deviceSessionToken = await AsyncStorage.getItem(DEVICE_SESSION_TOKEN_KEY);
|
|
36
|
+
if (deviceSessionToken) {
|
|
37
|
+
config.headers['X-Device-Session-Token'] = deviceSessionToken;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return config;
|
|
41
|
+
} catch (error) {
|
|
42
|
+
console.error('API Request Error:', error);
|
|
43
|
+
return config;
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
(error) => {
|
|
47
|
+
console.error('API Request Setup Error:', error);
|
|
48
|
+
return Promise.reject(error);
|
|
49
|
+
}
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
// Response interceptor for handling common responses and errors
|
|
53
|
+
api.interceptors.response.use(
|
|
54
|
+
(response) => response,
|
|
55
|
+
async (error) => {
|
|
56
|
+
// Handle 401 errors - session expired or invalid
|
|
57
|
+
if (error.response?.status === 401) {
|
|
58
|
+
<% if (features.authentication.enabled) { %>
|
|
59
|
+
// Sign out to clear BetterAuth session
|
|
60
|
+
try {
|
|
61
|
+
await authClient.signOut();
|
|
62
|
+
} catch {
|
|
63
|
+
// Ignore sign out errors
|
|
64
|
+
}
|
|
65
|
+
<% } %>
|
|
66
|
+
}
|
|
67
|
+
return Promise.reject(error);
|
|
68
|
+
}
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
export default api;
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
|
|
3
|
+
export interface AppError {
|
|
4
|
+
type: string;
|
|
5
|
+
message: string;
|
|
6
|
+
details?: any;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
class ErrorService {
|
|
10
|
+
private static instance: ErrorService;
|
|
11
|
+
|
|
12
|
+
private constructor() {}
|
|
13
|
+
|
|
14
|
+
public static getInstance(): ErrorService {
|
|
15
|
+
if (!ErrorService.instance) {
|
|
16
|
+
ErrorService.instance = new ErrorService();
|
|
17
|
+
}
|
|
18
|
+
return ErrorService.instance;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
handleAxiosError(error: any, context?: { [key: string]: any }): AppError {
|
|
22
|
+
if (axios.isAxiosError(error)) {
|
|
23
|
+
// Network error (no response)
|
|
24
|
+
if (!error.response) {
|
|
25
|
+
if (error.code === 'ECONNABORTED' || error.message.includes('timeout')) {
|
|
26
|
+
return {
|
|
27
|
+
type: 'TIMEOUT_ERROR',
|
|
28
|
+
message: 'Request timeout. Please try again.',
|
|
29
|
+
details: { context }
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
type: 'CONNECTION_ERROR',
|
|
34
|
+
message: 'Unable to connect. Please check your internet connection.',
|
|
35
|
+
details: { context }
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Server responded with error status
|
|
40
|
+
const status = error.response.status;
|
|
41
|
+
const data = error.response.data;
|
|
42
|
+
|
|
43
|
+
// Handle structured API errors
|
|
44
|
+
if (data?.error?.code && data?.error?.message) {
|
|
45
|
+
return {
|
|
46
|
+
type: data.error.code,
|
|
47
|
+
message: data.error.message,
|
|
48
|
+
details: data.error.details || context
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Handle common HTTP status codes
|
|
53
|
+
switch (status) {
|
|
54
|
+
case 400:
|
|
55
|
+
return {
|
|
56
|
+
type: 'VALIDATION_ERROR',
|
|
57
|
+
message: data?.message || 'Invalid request data.',
|
|
58
|
+
details: data?.details || context
|
|
59
|
+
};
|
|
60
|
+
case 401:
|
|
61
|
+
return {
|
|
62
|
+
type: 'AUTH_ERROR',
|
|
63
|
+
message: 'Please log in to continue.',
|
|
64
|
+
details: context
|
|
65
|
+
};
|
|
66
|
+
case 403:
|
|
67
|
+
return {
|
|
68
|
+
type: 'PERMISSION_ERROR',
|
|
69
|
+
message: 'You do not have permission to perform this action.',
|
|
70
|
+
details: context
|
|
71
|
+
};
|
|
72
|
+
case 404:
|
|
73
|
+
return {
|
|
74
|
+
type: 'NOT_FOUND_ERROR',
|
|
75
|
+
message: 'The requested resource was not found.',
|
|
76
|
+
details: context
|
|
77
|
+
};
|
|
78
|
+
case 409:
|
|
79
|
+
return {
|
|
80
|
+
type: 'CONFLICT_ERROR',
|
|
81
|
+
message: data?.message || 'A conflict occurred.',
|
|
82
|
+
details: context
|
|
83
|
+
};
|
|
84
|
+
case 429:
|
|
85
|
+
return {
|
|
86
|
+
type: 'RATE_LIMIT_ERROR',
|
|
87
|
+
message: 'Too many requests. Please try again later.',
|
|
88
|
+
details: context
|
|
89
|
+
};
|
|
90
|
+
case 500:
|
|
91
|
+
return {
|
|
92
|
+
type: 'SERVER_ERROR',
|
|
93
|
+
message: 'An internal server error occurred.',
|
|
94
|
+
details: context
|
|
95
|
+
};
|
|
96
|
+
case 503:
|
|
97
|
+
return {
|
|
98
|
+
type: 'SERVICE_UNAVAILABLE',
|
|
99
|
+
message: 'The service is temporarily unavailable.',
|
|
100
|
+
details: context
|
|
101
|
+
};
|
|
102
|
+
default:
|
|
103
|
+
return {
|
|
104
|
+
type: 'HTTP_ERROR',
|
|
105
|
+
message: data?.message || `Request failed with status ${status}`,
|
|
106
|
+
details: { status, context }
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return this.handleGenericError(error, context);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
handleGenericError(error: any, context?: { [key: string]: any }): AppError {
|
|
115
|
+
if (error instanceof Error) {
|
|
116
|
+
return {
|
|
117
|
+
type: 'GENERIC_ERROR',
|
|
118
|
+
message: error.message,
|
|
119
|
+
details: { context }
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
type: 'UNKNOWN_ERROR',
|
|
125
|
+
message: 'An unexpected error occurred.',
|
|
126
|
+
details: { originalError: String(error), context }
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
getUserFriendlyMessage(error: AppError): string {
|
|
131
|
+
const suggestions: { [key: string]: string } = {
|
|
132
|
+
CONNECTION_ERROR: 'Please check your internet connection and try again.',
|
|
133
|
+
TIMEOUT_ERROR: 'The request is taking longer than usual. Please try again.',
|
|
134
|
+
AUTH_ERROR: 'Please log in again.',
|
|
135
|
+
PERMISSION_ERROR: 'You do not have permission to perform this action.',
|
|
136
|
+
VALIDATION_ERROR: 'Please check your input and try again.',
|
|
137
|
+
CONFLICT_ERROR: 'This action conflicts with existing data.',
|
|
138
|
+
RATE_LIMIT_ERROR: 'Please wait a moment before trying again.',
|
|
139
|
+
SERVER_ERROR: 'Something went wrong on our end. Please try again later.',
|
|
140
|
+
SERVICE_UNAVAILABLE: 'The service is temporarily unavailable. Please try again later.',
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
return suggestions[error.type] || error.message;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
getSuggestions(error: AppError): string[] {
|
|
147
|
+
const suggestionMap: { [key: string]: string[] } = {
|
|
148
|
+
CONNECTION_ERROR: [
|
|
149
|
+
'Check your internet connection',
|
|
150
|
+
'Try switching between Wi-Fi and cellular data',
|
|
151
|
+
'Move to an area with better signal'
|
|
152
|
+
],
|
|
153
|
+
TIMEOUT_ERROR: [
|
|
154
|
+
'Try again in a few moments',
|
|
155
|
+
'Check your internet connection',
|
|
156
|
+
'The server might be experiencing high load'
|
|
157
|
+
],
|
|
158
|
+
AUTH_ERROR: [
|
|
159
|
+
'Please log in again',
|
|
160
|
+
'Check if your session has expired',
|
|
161
|
+
'Try logging out and back in'
|
|
162
|
+
],
|
|
163
|
+
VALIDATION_ERROR: [
|
|
164
|
+
'Double-check your input',
|
|
165
|
+
'Make sure all required fields are filled',
|
|
166
|
+
'Check the format of your data'
|
|
167
|
+
],
|
|
168
|
+
SERVER_ERROR: [
|
|
169
|
+
'Try again in a few minutes',
|
|
170
|
+
'Contact support if the problem persists',
|
|
171
|
+
'Check if there are any ongoing service issues'
|
|
172
|
+
]
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
return suggestionMap[error.type] || ['Please try again', 'Contact support if the problem persists'];
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export const errorService = ErrorService.getInstance();
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<% if (integrations.att.enabled) { %>import { Platform } from 'react-native';
|
|
2
|
+
import { useATTStore } from '@/store/att.store';
|
|
3
|
+
<% } %><% if (integrations.revenueCat.enabled) { %>import { revenueCatService } from './revenuecatService';
|
|
4
|
+
<% } %><% if (integrations.adjust.enabled) { %>import { adjustService } from './adjustService';
|
|
5
|
+
<% } %><% if (integrations.scate.enabled) { %>import { scateService } from './scateService';
|
|
6
|
+
<% } %>
|
|
7
|
+
export const initializeSDKs = async () => {
|
|
8
|
+
try {
|
|
9
|
+
<% if (integrations.revenueCat.enabled) { %> // Initialize RevenueCat
|
|
10
|
+
revenueCatService.initialize();
|
|
11
|
+
<% } %>
|
|
12
|
+
<% if (integrations.adjust.enabled) { %> // Initialize Adjust
|
|
13
|
+
<% if (integrations.att.enabled) { %> // Handle ATT permissions on iOS first
|
|
14
|
+
if (Platform.OS === 'ios') {
|
|
15
|
+
const attStore = useATTStore.getState();
|
|
16
|
+
await attStore.initialize();
|
|
17
|
+
await attStore.requestPermissions();
|
|
18
|
+
}
|
|
19
|
+
<% } %> adjustService.initialize();
|
|
20
|
+
<% } %>
|
|
21
|
+
<% if (integrations.scate.enabled) { %> // Initialize Scate
|
|
22
|
+
scateService.initialize();
|
|
23
|
+
<% } %>
|
|
24
|
+
<% if (integrations.adjust.enabled && (integrations.revenueCat.enabled || integrations.scate.enabled)) { %>
|
|
25
|
+
// Distribute ADID to other SDKs
|
|
26
|
+
const adid = await adjustService.getAdid();
|
|
27
|
+
if (adid) {
|
|
28
|
+
<% if (integrations.revenueCat.enabled) { %> // Set Adjust ID in RevenueCat
|
|
29
|
+
revenueCatService.setAdjustId(adid);
|
|
30
|
+
<% } %><% if (integrations.scate.enabled) { %> // Set ADID in Scate
|
|
31
|
+
scateService.setAdid(adid);
|
|
32
|
+
<% } %> }
|
|
33
|
+
<% } %> } catch (error) {
|
|
34
|
+
console.error('SDK initialization error:', error);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// UI Store (always included)
|
|
2
|
+
export * from './ui.store';
|
|
3
|
+
|
|
4
|
+
<% if (features.sessionManagement) { %>// Device Session Store
|
|
5
|
+
export * from './deviceSession.store';
|
|
6
|
+
<% } %>
|
|
7
|
+
<% if (integrations.revenueCat.enabled) { %>// RevenueCat Store
|
|
8
|
+
export * from './revenuecat.store';
|
|
9
|
+
<% } %>
|
|
10
|
+
<% if (integrations.adjust.enabled) { %>// Adjust Store
|
|
11
|
+
export * from './adjust.store';
|
|
12
|
+
<% } %>
|
|
13
|
+
<% if (integrations.scate.enabled) { %>// Scate Store
|
|
14
|
+
export * from './scate.store';
|
|
15
|
+
<% } %>
|
|
16
|
+
<% if (integrations.att.enabled) { %>// ATT Store
|
|
17
|
+
export * from './att.store';
|
|
18
|
+
<% } %>
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { create } from 'zustand';
|
|
2
|
+
|
|
3
|
+
interface UIState {
|
|
4
|
+
// Global loading states
|
|
5
|
+
isAppLoading: boolean;
|
|
6
|
+
isNetworkLoading: boolean;
|
|
7
|
+
|
|
8
|
+
// Theme
|
|
9
|
+
colorScheme: 'light' | 'dark' | 'system';
|
|
10
|
+
|
|
11
|
+
// Notifications/Toasts
|
|
12
|
+
notification: {
|
|
13
|
+
id: string;
|
|
14
|
+
type: 'success' | 'error' | 'warning' | 'info';
|
|
15
|
+
title: string;
|
|
16
|
+
message?: string;
|
|
17
|
+
duration?: number;
|
|
18
|
+
} | null;
|
|
19
|
+
|
|
20
|
+
// Actions
|
|
21
|
+
setAppLoading: (loading: boolean) => void;
|
|
22
|
+
setNetworkLoading: (loading: boolean) => void;
|
|
23
|
+
setColorScheme: (scheme: 'light' | 'dark' | 'system') => void;
|
|
24
|
+
showNotification: (notification: {
|
|
25
|
+
type: 'success' | 'error' | 'warning' | 'info';
|
|
26
|
+
title: string;
|
|
27
|
+
message?: string;
|
|
28
|
+
duration?: number;
|
|
29
|
+
}) => void;
|
|
30
|
+
hideNotification: () => void;
|
|
31
|
+
|
|
32
|
+
// Computed
|
|
33
|
+
isAnyLoading: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const useUIStore = create<UIState>((set, get) => ({
|
|
37
|
+
// Initial state
|
|
38
|
+
isAppLoading: false,
|
|
39
|
+
isNetworkLoading: false,
|
|
40
|
+
colorScheme: 'system',
|
|
41
|
+
notification: null,
|
|
42
|
+
|
|
43
|
+
// Actions
|
|
44
|
+
setAppLoading: (isAppLoading) => set({ isAppLoading }),
|
|
45
|
+
setNetworkLoading: (isNetworkLoading) => set({ isNetworkLoading }),
|
|
46
|
+
setColorScheme: (colorScheme) => set({ colorScheme }),
|
|
47
|
+
|
|
48
|
+
showNotification: (notificationData) => {
|
|
49
|
+
const id = `notification-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
50
|
+
set({
|
|
51
|
+
notification: {
|
|
52
|
+
id,
|
|
53
|
+
...notificationData,
|
|
54
|
+
duration: notificationData.duration ?? 4000,
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Auto-hide after duration
|
|
59
|
+
const duration = notificationData.duration ?? 4000;
|
|
60
|
+
if (duration > 0) {
|
|
61
|
+
setTimeout(() => {
|
|
62
|
+
const currentNotification = get().notification;
|
|
63
|
+
if (currentNotification?.id === id) {
|
|
64
|
+
set({ notification: null });
|
|
65
|
+
}
|
|
66
|
+
}, duration);
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
|
|
70
|
+
hideNotification: () => set({ notification: null }),
|
|
71
|
+
|
|
72
|
+
// Computed values
|
|
73
|
+
get isAnyLoading() {
|
|
74
|
+
const state = get();
|
|
75
|
+
return state.isAppLoading || state.isNetworkLoading;
|
|
76
|
+
},
|
|
77
|
+
}));
|
|
78
|
+
|
|
79
|
+
// Selectors for commonly used UI state
|
|
80
|
+
export const useUI = () => {
|
|
81
|
+
const state = useUIStore();
|
|
82
|
+
return {
|
|
83
|
+
isAppLoading: state.isAppLoading,
|
|
84
|
+
isNetworkLoading: state.isNetworkLoading,
|
|
85
|
+
isAnyLoading: state.isAnyLoading,
|
|
86
|
+
colorScheme: state.colorScheme,
|
|
87
|
+
notification: state.notification,
|
|
88
|
+
};
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export const useUIActions = () => {
|
|
92
|
+
const state = useUIStore();
|
|
93
|
+
return {
|
|
94
|
+
setAppLoading: state.setAppLoading,
|
|
95
|
+
setNetworkLoading: state.setNetworkLoading,
|
|
96
|
+
setColorScheme: state.setColorScheme,
|
|
97
|
+
showNotification: state.showNotification,
|
|
98
|
+
hideNotification: state.hideNotification,
|
|
99
|
+
};
|
|
100
|
+
};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Format date strings for display
|
|
3
|
+
*/
|
|
4
|
+
export const formatDate = (dateString: string, options?: Intl.DateTimeFormatOptions): string => {
|
|
5
|
+
try {
|
|
6
|
+
const date = new Date(dateString);
|
|
7
|
+
if (isNaN(date.getTime())) {
|
|
8
|
+
return dateString; // Return original if invalid
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const defaultOptions: Intl.DateTimeFormatOptions = {
|
|
12
|
+
year: 'numeric',
|
|
13
|
+
month: 'short',
|
|
14
|
+
day: 'numeric',
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
return date.toLocaleDateString(undefined, options || defaultOptions);
|
|
18
|
+
} catch (error) {
|
|
19
|
+
return dateString; // Return original if formatting fails
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Format relative time (e.g., "2 hours ago")
|
|
25
|
+
*/
|
|
26
|
+
export const formatRelativeTime = (dateString: string): string => {
|
|
27
|
+
try {
|
|
28
|
+
const date = new Date(dateString);
|
|
29
|
+
const now = new Date();
|
|
30
|
+
const diffInSeconds = (now.getTime() - date.getTime()) / 1000;
|
|
31
|
+
|
|
32
|
+
if (diffInSeconds < 60) {
|
|
33
|
+
return 'Just now';
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const diffInMinutes = Math.floor(diffInSeconds / 60);
|
|
37
|
+
if (diffInMinutes < 60) {
|
|
38
|
+
return `${diffInMinutes} ${diffInMinutes === 1 ? 'minute' : 'minutes'} ago`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const diffInHours = Math.floor(diffInMinutes / 60);
|
|
42
|
+
if (diffInHours < 24) {
|
|
43
|
+
return `${diffInHours} ${diffInHours === 1 ? 'hour' : 'hours'} ago`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const diffInDays = Math.floor(diffInHours / 24);
|
|
47
|
+
if (diffInDays < 7) {
|
|
48
|
+
return `${diffInDays} ${diffInDays === 1 ? 'day' : 'days'} ago`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// For longer periods, show the actual date
|
|
52
|
+
return formatDate(dateString);
|
|
53
|
+
} catch (error) {
|
|
54
|
+
return dateString;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Capitalize first letter of each word
|
|
60
|
+
*/
|
|
61
|
+
export const capitalizeWords = (str: string): string => {
|
|
62
|
+
return str.replace(/\b\w/g, l => l.toUpperCase());
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Truncate text with ellipsis
|
|
67
|
+
*/
|
|
68
|
+
export const truncateText = (text: string, maxLength: number): string => {
|
|
69
|
+
if (text.length <= maxLength) return text;
|
|
70
|
+
return text.slice(0, maxLength - 3) + '...';
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Format email for display (hide middle part)
|
|
75
|
+
*/
|
|
76
|
+
export const formatEmailForDisplay = (email: string): string => {
|
|
77
|
+
const [localPart, domain] = email.split('@');
|
|
78
|
+
if (!domain) return email;
|
|
79
|
+
|
|
80
|
+
if (localPart.length <= 2) return email;
|
|
81
|
+
|
|
82
|
+
const visibleChars = Math.max(1, Math.floor(localPart.length * 0.3));
|
|
83
|
+
const hiddenPart = '*'.repeat(localPart.length - visibleChars * 2);
|
|
84
|
+
const maskedLocal = localPart.slice(0, visibleChars) + hiddenPart + localPart.slice(-visibleChars);
|
|
85
|
+
|
|
86
|
+
return `${maskedLocal}@${domain}`;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Validate email format
|
|
91
|
+
*/
|
|
92
|
+
export const isValidEmail = (email: string): boolean => {
|
|
93
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
94
|
+
return emailRegex.test(email);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Format name for display
|
|
99
|
+
*/
|
|
100
|
+
export const formatDisplayName = (name: string | null | undefined, fallback: string = 'User'): string => {
|
|
101
|
+
if (!name || !name.trim()) {
|
|
102
|
+
return fallback;
|
|
103
|
+
}
|
|
104
|
+
return name.trim();
|
|
105
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
interface LogLevel {
|
|
2
|
+
DEBUG: number;
|
|
3
|
+
INFO: number;
|
|
4
|
+
WARN: number;
|
|
5
|
+
ERROR: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const LOG_LEVELS: LogLevel = {
|
|
9
|
+
DEBUG: 0,
|
|
10
|
+
INFO: 1,
|
|
11
|
+
WARN: 2,
|
|
12
|
+
ERROR: 3,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
class Logger {
|
|
16
|
+
private currentLevel: number;
|
|
17
|
+
|
|
18
|
+
constructor() {
|
|
19
|
+
// Set log level based on environment
|
|
20
|
+
this.currentLevel = __DEV__ ? LOG_LEVELS.DEBUG : LOG_LEVELS.WARN;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
private shouldLog(level: number): boolean {
|
|
24
|
+
return level >= this.currentLevel;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
private formatMessage(level: string, message: string, data?: any): string {
|
|
28
|
+
const timestamp = new Date().toISOString();
|
|
29
|
+
const prefix = `[${timestamp}] [${level}]`;
|
|
30
|
+
|
|
31
|
+
if (data) {
|
|
32
|
+
return `${prefix} ${message}\n${JSON.stringify(data, null, 2)}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return `${prefix} ${message}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
debug(message: string, data?: any): void {
|
|
39
|
+
if (this.shouldLog(LOG_LEVELS.DEBUG)) {
|
|
40
|
+
console.log(this.formatMessage('DEBUG', message, data));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
info(message: string, data?: any): void {
|
|
45
|
+
if (this.shouldLog(LOG_LEVELS.INFO)) {
|
|
46
|
+
console.info(this.formatMessage('INFO', message, data));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
warn(message: string, data?: any): void {
|
|
51
|
+
if (this.shouldLog(LOG_LEVELS.WARN)) {
|
|
52
|
+
console.warn(this.formatMessage('WARN', message, data));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
error(message: string, data?: any): void {
|
|
57
|
+
if (this.shouldLog(LOG_LEVELS.ERROR)) {
|
|
58
|
+
console.error(this.formatMessage('ERROR', message, data));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Convenience method for API logging
|
|
63
|
+
apiRequest(method: string, url: string, data?: any): void {
|
|
64
|
+
this.debug(`API ${method.toUpperCase()} ${url}`, data);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
apiResponse(method: string, url: string, status: number, data?: any): void {
|
|
68
|
+
const level = status >= 400 ? 'error' : 'debug';
|
|
69
|
+
this[level](`API ${method.toUpperCase()} ${url} - ${status}`, data);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export const logger = new Logger();
|