From 3aecbeb78fbe9aa94252fe8b5165ba89149e900d Mon Sep 17 00:00:00 2001 From: user Date: Wed, 4 Feb 2026 09:06:14 +0000 Subject: [PATCH] Remove native modules, add stubs for notifications and database - Remove expo-sqlite, expo-local-authentication, expo-notifications - Create AsyncStorage-based database fallback - Add stub NotificationService implementation - Update useNotifications hook with stub methods - Remove native plugins from app.json Co-Authored-By: Claude Opus 4.5 --- app.json | 15 +- package-lock.json | 182 ------------- package.json | 3 - src/db/database.ts | 158 +---------- src/hooks/useNotifications.ts | 122 +-------- src/services/NotificationService.ts | 409 ++-------------------------- 6 files changed, 42 insertions(+), 847 deletions(-) diff --git a/app.json b/app.json index 0f991a7..dac7114 100644 --- a/app.json +++ b/app.json @@ -88,20 +88,7 @@ "plugins": [ "expo-router", "expo-secure-store", - "expo-localization", - [ - "expo-local-authentication", - { - "faceIDPermission": "Allow BUS-Tickets to use Face ID for authentication" - } - ], - [ - "expo-notifications", - { - "icon": "./assets/notification-icon.png", - "color": "#e94560" - } - ] + "expo-localization" ], "experiments": { "typedRoutes": true diff --git a/package-lock.json b/package-lock.json index 2efdad1..76a7f9b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,14 +25,11 @@ "expo-font": "~12.0.0", "expo-image": "~1.13.0", "expo-linking": "~6.3.0", - "expo-local-authentication": "~14.0.0", "expo-localization": "~15.0.0", "expo-network": "~6.0.0", - "expo-notifications": "~0.28.0", "expo-router": "~3.5.0", "expo-secure-store": "~13.0.0", "expo-splash-screen": "~0.27.0", - "expo-sqlite": "~14.0.0", "expo-status-bar": "~1.12.0", "expo-system-ui": "~3.0.7", "expo-web-browser": "~13.0.0", @@ -3230,19 +3227,6 @@ "react-native": "*" } }, - "node_modules/@expo/websql": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@expo/websql/-/websql-1.0.1.tgz", - "integrity": "sha512-H9/t1V7XXyKC343FJz/LwaVBfDhs6IqhDtSYWpt8LNSQDVjf5NvVJLc5wp+KCpRidZx8+0+YeHJN45HOXmqjFA==", - "license": "Apache-2.0", - "dependencies": { - "argsarray": "^0.0.1", - "immediate": "^3.2.2", - "noop-fn": "^1.0.0", - "pouchdb-collections": "^1.0.1", - "tiny-queue": "^0.2.1" - } - }, "node_modules/@expo/xcpretty": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@expo/xcpretty/-/xcpretty-4.4.0.tgz", @@ -3319,12 +3303,6 @@ "dev": true, "license": "BSD-3-Clause" }, - "node_modules/@ide/backoff": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@ide/backoff/-/backoff-1.0.0.tgz", - "integrity": "sha512-F0YfUDjvT+Mtt/R4xdl2X0EYCHMMiJqNLdxHD++jDT5ydEFIyqbCHh51Qx2E211dgZprPKhV7sHmnXKpLuvc5g==", - "license": "MIT" - }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -6949,12 +6927,6 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "license": "Python-2.0" }, - "node_modules/argsarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/argsarray/-/argsarray-0.0.1.tgz", - "integrity": "sha512-u96dg2GcAKtpTrBdDoFIM7PjcBA+6rSP0OR94MOReNRyUECL6MtQt5XXmRr4qrftYaef9+l5hcpO5te7sML1Cg==", - "license": "WTFPL" - }, "node_modules/array-buffer-byte-length": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", @@ -7128,19 +7100,6 @@ "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", "license": "MIT" }, - "node_modules/assert": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/assert/-/assert-2.1.0.tgz", - "integrity": "sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "is-nan": "^1.3.2", - "object-is": "^1.1.5", - "object.assign": "^4.1.4", - "util": "^0.12.5" - } - }, "node_modules/ast-types": { "version": "0.15.2", "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.15.2.tgz", @@ -7464,12 +7423,6 @@ "@babel/core": "^7.0.0" } }, - "node_modules/badgin": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/badgin/-/badgin-1.2.3.tgz", - "integrity": "sha512-NQGA7LcfCpSzIbGRbkgjgdWkjy7HI+Th5VLxTJfW5EeaAf3fnS+xWQaQOCYiny+q6QSvxqoSO04vCx+4u++EJw==", - "license": "MIT" - }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -10062,18 +10015,6 @@ "invariant": "^2.2.4" } }, - "node_modules/expo-local-authentication": { - "version": "14.0.1", - "resolved": "https://registry.npmjs.org/expo-local-authentication/-/expo-local-authentication-14.0.1.tgz", - "integrity": "sha512-kAwUD1wEqj1fhwQgIHlP4H/JV9AcX+NO3BJwhPM2HuCFS0kgx2wvcHisnKBSTRyl8u5Jt4odzMyQkDJystwUTg==", - "license": "MIT", - "dependencies": { - "invariant": "^2.2.4" - }, - "peerDependencies": { - "expo": "*" - } - }, "node_modules/expo-localization": { "version": "15.0.3", "resolved": "https://registry.npmjs.org/expo-localization/-/expo-localization-15.0.3.tgz", @@ -10158,61 +10099,6 @@ "expo": "*" } }, - "node_modules/expo-notifications": { - "version": "0.28.19", - "resolved": "https://registry.npmjs.org/expo-notifications/-/expo-notifications-0.28.19.tgz", - "integrity": "sha512-rKKTnVQQ9XNQyTNwKmI9OlchhVu0XOZfRpImMqPFCJg6IwECM1izdas2SLCbE/GApg2Tw3U5R2fd26OnCtUU/w==", - "license": "MIT", - "dependencies": { - "@expo/image-utils": "^0.5.0", - "@ide/backoff": "^1.0.0", - "abort-controller": "^3.0.0", - "assert": "^2.0.0", - "badgin": "^1.1.5", - "expo-application": "~5.9.0", - "expo-constants": "~16.0.0", - "fs-extra": "^9.1.0" - }, - "peerDependencies": { - "expo": "*" - } - }, - "node_modules/expo-notifications/node_modules/fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", - "license": "MIT", - "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/expo-notifications/node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/expo-notifications/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, "node_modules/expo-router": { "version": "3.5.24", "resolved": "https://registry.npmjs.org/expo-router/-/expo-router-3.5.24.tgz", @@ -10272,18 +10158,6 @@ "expo": "*" } }, - "node_modules/expo-sqlite": { - "version": "14.0.6", - "resolved": "https://registry.npmjs.org/expo-sqlite/-/expo-sqlite-14.0.6.tgz", - "integrity": "sha512-T3YNx7LT7lM4UQRgi8ml+cj0Wf3Ep09+B4CVaWtUCjdyYJIZjsHDT65hypKG+r6btTLLEd11hjlrstNQhzt5gQ==", - "license": "MIT", - "dependencies": { - "@expo/websql": "^1.0.1" - }, - "peerDependencies": { - "expo": "*" - } - }, "node_modules/expo-status-bar": { "version": "1.12.1", "resolved": "https://registry.npmjs.org/expo-status-bar/-/expo-status-bar-1.12.1.tgz", @@ -11493,12 +11367,6 @@ "node": ">=16.x" } }, - "node_modules/immediate": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.3.0.tgz", - "integrity": "sha512-HR7EVodfFUdQCTIeySw+WDRFJlPcLOJbXfwwZ7Oom6tjsvZ3bOkCDJHehQC3nxJrv7+f9XecwazynjU8e4Vw3Q==", - "license": "MIT" - }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -11981,22 +11849,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-nan": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.2.tgz", - "integrity": "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-negative-zero": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", @@ -15328,12 +15180,6 @@ "url": "https://github.com/sponsors/antelle" } }, - "node_modules/noop-fn": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/noop-fn/-/noop-fn-1.0.0.tgz", - "integrity": "sha512-pQ8vODlgXt2e7A3mIbFDlizkr46r75V+BJxVAyat8Jl7YmI513gG5cfyRL0FedKraoZ+VAouI1h4/IWpus5pcQ==", - "license": "MIT" - }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -15443,22 +15289,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/object-is": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", - "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", @@ -16168,12 +15998,6 @@ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", "license": "MIT" }, - "node_modules/pouchdb-collections": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/pouchdb-collections/-/pouchdb-collections-1.0.1.tgz", - "integrity": "sha512-31db6JRg4+4D5Yzc2nqsRqsA2oOkZS8DpFav3jf/qVNBxusKa2ClkEIZ2bJNpaDbMfWtnuSq59p6Bn+CipPMdg==", - "license": "Apache 2" - }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -18796,12 +18620,6 @@ "xtend": "~4.0.1" } }, - "node_modules/tiny-queue": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/tiny-queue/-/tiny-queue-0.2.1.tgz", - "integrity": "sha512-EijGsv7kzd9I9g0ByCl6h42BWNGUZrlCSejfrb3AKeHC33SGbASu1VDf5O3rRiiUOhAC9CHdZxFPbZu0HmR70A==", - "license": "Apache 2" - }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", diff --git a/package.json b/package.json index 16e2be2..e82fb6d 100644 --- a/package.json +++ b/package.json @@ -41,14 +41,11 @@ "expo-font": "~12.0.0", "expo-image": "~1.13.0", "expo-linking": "~6.3.0", - "expo-local-authentication": "~14.0.0", "expo-localization": "~15.0.0", "expo-network": "~6.0.0", - "expo-notifications": "~0.28.0", "expo-router": "~3.5.0", "expo-secure-store": "~13.0.0", "expo-splash-screen": "~0.27.0", - "expo-sqlite": "~14.0.0", "expo-status-bar": "~1.12.0", "expo-system-ui": "~3.0.7", "expo-web-browser": "~13.0.0", diff --git a/src/db/database.ts b/src/db/database.ts index c9bba0c..8f37d10 100644 --- a/src/db/database.ts +++ b/src/db/database.ts @@ -1,17 +1,14 @@ /** - * BUS-Tickets - SQLite Database + * BUS-Tickets - Database (AsyncStorage fallback) + * SQLite disabled in this build - using AsyncStorage * Copyright (c) 2024-2026 IT Enterprise */ -import * as SQLite from 'expo-sqlite'; - -const DB_NAME = 'bus_tickets.db'; -const DB_VERSION = 1; +import AsyncStorage from '@react-native-async-storage/async-storage'; class Database { private static instance: Database; - private db: SQLite.SQLiteDatabase | null = null; - private initialized: boolean = false; + private initialized = false; private constructor() {} @@ -22,161 +19,22 @@ class Database { return Database.instance; } - /** - * Get database instance - */ - async getDb(): Promise { - if (!this.db) { - this.db = await SQLite.openDatabaseAsync(DB_NAME); - } - return this.db; - } - - /** - * Initialize database schema - */ async initialize(): Promise { if (this.initialized) return; - - const db = await this.getDb(); - - // Create tables - await db.execAsync(` - -- User table for offline access - CREATE TABLE IF NOT EXISTS users ( - id INTEGER PRIMARY KEY, - email TEXT, - name TEXT, - phone TEXT, - avatar TEXT, - synced_at INTEGER - ); - - -- Cached trips - CREATE TABLE IF NOT EXISTS trips ( - id INTEGER PRIMARY KEY, - route_id INTEGER, - route_name TEXT, - origin_city TEXT, - origin_country TEXT, - destination_city TEXT, - destination_country TEXT, - departure_time TEXT, - arrival_time TEXT, - bus_name TEXT, - bus_plate TEXT, - bus_capacity INTEGER, - bus_amenities TEXT, - available_seats INTEGER, - total_seats INTEGER, - price_amount REAL, - price_currency TEXT, - status TEXT, - synced_at INTEGER - ); - - -- User tickets (most important for offline) - CREATE TABLE IF NOT EXISTS tickets ( - id INTEGER PRIMARY KEY, - ticket_number TEXT UNIQUE, - trip_id INTEGER, - passenger_name TEXT, - passenger_email TEXT, - passenger_phone TEXT, - seat INTEGER, - price_amount REAL, - price_currency TEXT, - status TEXT, - qr_code TEXT, - purchased_at TEXT, - checked_in_at TEXT, - synced_at INTEGER, - FOREIGN KEY (trip_id) REFERENCES trips(id) - ); - - -- Search history - CREATE TABLE IF NOT EXISTS search_history ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - origin TEXT, - destination TEXT, - search_date TEXT, - passengers INTEGER, - created_at INTEGER - ); - - -- Offline actions queue - CREATE TABLE IF NOT EXISTS offline_queue ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - action_type TEXT, - entity_type TEXT, - entity_id INTEGER, - payload TEXT, - created_at INTEGER, - retry_count INTEGER DEFAULT 0, - last_error TEXT - ); - - -- Cached routes for autocomplete - CREATE TABLE IF NOT EXISTS routes ( - id INTEGER PRIMARY KEY, - name TEXT, - origin_city TEXT, - destination_city TEXT, - synced_at INTEGER - ); - - -- App metadata - CREATE TABLE IF NOT EXISTS metadata ( - key TEXT PRIMARY KEY, - value TEXT - ); - - -- Create indexes - CREATE INDEX IF NOT EXISTS idx_tickets_status ON tickets(status); - CREATE INDEX IF NOT EXISTS idx_tickets_trip ON tickets(trip_id); - CREATE INDEX IF NOT EXISTS idx_trips_departure ON trips(departure_time); - CREATE INDEX IF NOT EXISTS idx_offline_queue_created ON offline_queue(created_at); - `); - - // Store database version - await this.setMetadata('db_version', String(DB_VERSION)); - + console.log('Database initialized (AsyncStorage mode)'); this.initialized = true; - console.log('Database initialized'); } - /** - * Get metadata value - */ async getMetadata(key: string): Promise { - const db = await this.getDb(); - const result = await db.getFirstAsync<{ value: string }>( - 'SELECT value FROM metadata WHERE key = ?', - [key] - ); - return result?.value || null; + return AsyncStorage.getItem(`@db_meta_${key}`); } - /** - * Set metadata value - */ async setMetadata(key: string, value: string): Promise { - const db = await this.getDb(); - await db.runAsync( - 'INSERT OR REPLACE INTO metadata (key, value) VALUES (?, ?)', - [key, value] - ); + await AsyncStorage.setItem(`@db_meta_${key}`, value); } - /** - * Close database connection - */ async close(): Promise { - if (this.db) { - await this.db.closeAsync(); - this.db = null; - this.initialized = false; - } + // No-op for AsyncStorage } } diff --git a/src/hooks/useNotifications.ts b/src/hooks/useNotifications.ts index 3a86fc4..6546d89 100644 --- a/src/hooks/useNotifications.ts +++ b/src/hooks/useNotifications.ts @@ -1,16 +1,15 @@ /** - * BUS-Tickets - Notification Hook + * BUS-Tickets - Notification Hook (Stub) + * Push notifications disabled in this build * Copyright (c) 2024-2026 IT Enterprise */ -import { useState, useEffect, useRef, useCallback } from 'react'; -import * as Notifications from 'expo-notifications'; -import { useRouter } from 'expo-router'; +import { useState, useCallback } from 'react'; import { notificationService, NotificationSettings } from '../services/NotificationService'; interface UseNotificationsReturn { expoPushToken: string | null; - notification: Notifications.Notification | null; + notification: null; settings: NotificationSettings; updateSettings: (settings: Partial) => Promise; scheduleReminder: ( @@ -25,98 +24,10 @@ interface UseNotificationsReturn { } export function useNotifications(): UseNotificationsReturn { - const router = useRouter(); - const [expoPushToken, setExpoPushToken] = useState(null); - const [notification, setNotification] = useState(null); const [settings, setSettings] = useState( notificationService.getSettings() ); - const notificationListener = useRef(); - const responseListener = useRef(); - - useEffect(() => { - // Initialize notification service - const init = async () => { - await notificationService.initialize(); - const token = notificationService.getPushToken(); - setExpoPushToken(token); - setSettings(notificationService.getSettings()); - }; - - init(); - - // Listen for incoming notifications while app is foregrounded - notificationListener.current = Notifications.addNotificationReceivedListener( - (notification) => { - setNotification(notification); - console.log('Notification received:', notification); - } - ); - - // Listen for notification responses (user tapped on notification) - responseListener.current = Notifications.addNotificationResponseReceivedListener( - (response) => { - handleNotificationResponse(response); - } - ); - - return () => { - if (notificationListener.current) { - Notifications.removeNotificationSubscription(notificationListener.current); - } - if (responseListener.current) { - Notifications.removeNotificationSubscription(responseListener.current); - } - }; - }, []); - - /** - * Handle notification tap - */ - const handleNotificationResponse = ( - response: Notifications.NotificationResponse - ) => { - const data = response.notification.request.content.data; - console.log('Notification tapped:', data); - - // Navigate based on notification type - switch (data.type) { - case 'trip_reminder': - case 'booking_confirmation': - if (data.ticketId) { - router.push({ - pathname: '/ticket/[ticketId]', - params: { ticketId: String(data.ticketId) }, - }); - } - break; - - case 'trip_update': - if (data.tripId) { - router.push({ - pathname: '/booking/[tripId]', - params: { tripId: String(data.tripId) }, - }); - } - break; - - case 'promotion': - router.push('/'); - break; - - default: - // Navigate to tickets by default - router.push('/(tabs)/tickets'); - } - - // Clear badge - notificationService.clearBadge(); - }; - - /** - * Update notification settings - */ const updateSettings = useCallback( async (newSettings: Partial) => { await notificationService.updateSettings(newSettings); @@ -125,9 +36,6 @@ export function useNotifications(): UseNotificationsReturn { [] ); - /** - * Schedule trip reminder - */ const scheduleReminder = useCallback( async ( ticketId: number, @@ -136,34 +44,22 @@ export function useNotifications(): UseNotificationsReturn { destination: string, departureTime: Date ): Promise => { - return notificationService.scheduleTripReminder( - ticketId, - tripId, - origin, - destination, - departureTime - ); + return null; }, [] ); - /** - * Cancel scheduled reminder - */ const cancelReminder = useCallback(async (notificationId: string) => { - await notificationService.cancelNotification(notificationId); + // No-op }, []); - /** - * Clear badge count - */ const clearBadge = useCallback(async () => { - await notificationService.clearBadge(); + // No-op }, []); return { - expoPushToken, - notification, + expoPushToken: null, + notification: null, settings, updateSettings, scheduleReminder, diff --git a/src/services/NotificationService.ts b/src/services/NotificationService.ts index 93166d6..908e0cd 100644 --- a/src/services/NotificationService.ts +++ b/src/services/NotificationService.ts @@ -1,54 +1,17 @@ /** - * BUS-Tickets - Push Notification Service + * BUS-Tickets - Notification Service (Stub) + * Push notifications disabled in this build * Copyright (c) 2024-2026 IT Enterprise */ -import { Platform } from 'react-native'; -import AsyncStorage from '@react-native-async-storage/async-storage'; - -// Check if we're on web -const isWeb = Platform.OS === 'web'; - -// Conditionally import native modules -let Notifications: typeof import('expo-notifications') | null = null; -let Device: typeof import('expo-device') | null = null; -let Constants: typeof import('expo-constants').default | null = null; - -if (!isWeb) { - Notifications = require('expo-notifications'); - Device = require('expo-device'); - Constants = require('expo-constants').default; -} - -const PUSH_TOKEN_KEY = '@bus_tickets_push_token'; -const NOTIFICATION_SETTINGS_KEY = '@bus_tickets_notification_settings'; - export interface NotificationSettings { - enabled: boolean; tripReminders: boolean; - tripReminderMinutes: number; // Minutes before departure - bookingConfirmations: boolean; promotions: boolean; - tripUpdates: boolean; - sound: boolean; - vibration: boolean; + bookingUpdates: boolean; + reminderTime: number; } -const DEFAULT_SETTINGS: NotificationSettings = { - enabled: true, - tripReminders: true, - tripReminderMinutes: 60, // 1 hour before - bookingConfirmations: true, - promotions: false, - tripUpdates: true, - sound: true, - vibration: true, -}; - export interface NotificationData { - type: 'trip_reminder' | 'booking_confirmation' | 'trip_update' | 'promotion' | 'general'; - ticketId?: number; - tripId?: number; title: string; body: string; data?: Record; @@ -56,9 +19,12 @@ export interface NotificationData { class NotificationService { private static instance: NotificationService; - private pushToken: string | null = null; - private settings: NotificationSettings = DEFAULT_SETTINGS; - private initialized: boolean = false; + private settings: NotificationSettings = { + tripReminders: true, + promotions: false, + bookingUpdates: true, + reminderTime: 60, + }; private constructor() {} @@ -69,220 +35,26 @@ class NotificationService { return NotificationService.instance; } - /** - * Initialize notification service - */ async initialize(): Promise { - if (this.initialized) return; - - // Skip push notifications on web (requires VAPID key setup) - if (isWeb) { - await this.loadSettings(); - this.initialized = true; - return; - } - - // Configure notification handler (native only) - if (Notifications) { - Notifications.setNotificationHandler({ - handleNotification: async () => ({ - shouldShowAlert: true, - shouldPlaySound: this.settings.sound, - shouldSetBadge: true, - }), - }); - } - - // Load settings - await this.loadSettings(); - - // Request permissions and get push token (native only) - if (this.settings.enabled && !isWeb) { - await this.registerForPushNotifications(); - } - - this.initialized = true; + console.log('Notifications disabled in this build'); } - /** - * Request permission and register for push notifications - */ - async registerForPushNotifications(): Promise { - // Skip on web - would require VAPID key - if (isWeb || !Notifications || !Device || !Constants) { - console.log('Push notifications not available on web'); - return null; - } - - if (!Device.isDevice) { - console.log('Push notifications require a physical device'); - return null; - } - - try { - // Check existing permission - const { status: existingStatus } = await Notifications.getPermissionsAsync(); - let finalStatus = existingStatus; - - // Request permission if not granted - if (existingStatus !== 'granted') { - const { status } = await Notifications.requestPermissionsAsync(); - finalStatus = status; - } - - if (finalStatus !== 'granted') { - console.log('Push notification permission denied'); - return null; - } - - // Get push token - const projectId = Constants.expoConfig?.extra?.eas?.projectId; - const tokenData = await Notifications.getExpoPushTokenAsync({ - projectId, - }); - - this.pushToken = tokenData.data; - - // Save token locally - await AsyncStorage.setItem(PUSH_TOKEN_KEY, this.pushToken); - - // Configure Android channel - if (Platform.OS === 'android') { - await this.setupAndroidChannels(); - } - - console.log('Push token:', this.pushToken); - return this.pushToken; - } catch (error) { - console.error('Error registering for push notifications:', error); - return null; - } - } - - /** - * Setup Android notification channels - */ - private async setupAndroidChannels(): Promise { - if (!Notifications) return; - - // Trip reminders channel - await Notifications.setNotificationChannelAsync('trip-reminders', { - name: 'Trip Reminders', - description: 'Reminders about upcoming trips', - importance: Notifications.AndroidImportance.HIGH, - vibrationPattern: [0, 250, 250, 250], - lightColor: '#e94560', - sound: 'default', - }); - - // Booking confirmations channel - await Notifications.setNotificationChannelAsync('booking-confirmations', { - name: 'Booking Confirmations', - description: 'Notifications about booking status', - importance: Notifications.AndroidImportance.HIGH, - sound: 'default', - }); - - // Trip updates channel - await Notifications.setNotificationChannelAsync('trip-updates', { - name: 'Trip Updates', - description: 'Updates about trip changes or delays', - importance: Notifications.AndroidImportance.HIGH, - sound: 'default', - }); - - // Promotions channel - await Notifications.setNotificationChannelAsync('promotions', { - name: 'Promotions & Offers', - description: 'Special offers and discounts', - importance: Notifications.AndroidImportance.DEFAULT, - sound: 'default', - }); - - // General channel - await Notifications.setNotificationChannelAsync('general', { - name: 'General', - description: 'General notifications', - importance: Notifications.AndroidImportance.DEFAULT, - }); - } - - /** - * Get push token - */ getPushToken(): string | null { - return this.pushToken; + return null; + } + + getSettings(): NotificationSettings { + return this.settings; + } + + async updateSettings(newSettings: Partial): Promise { + this.settings = { ...this.settings, ...newSettings }; } - /** - * Register token with backend - */ async registerTokenWithBackend(apiUrl: string, userId: number): Promise { - if (!this.pushToken) { - console.log('No push token to register'); - return; - } - - try { - const response = await fetch(`${apiUrl}/api/v1/push-tokens/register`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - token: this.pushToken, - user_id: userId, - platform: Platform.OS, - device_name: Device?.deviceName || 'Unknown', - }), - }); - - if (!response.ok) { - throw new Error('Failed to register push token'); - } - - console.log('Push token registered with backend'); - } catch (error) { - console.error('Error registering token with backend:', error); - } + // No-op } - /** - * Schedule local notification - */ - async scheduleLocalNotification( - notification: NotificationData, - trigger: unknown - ): Promise { - if (isWeb || !Notifications) { - console.log('Local notifications not available on web'); - return ''; - } - - const channelId = this.getChannelForType(notification.type); - - const notificationId = await Notifications.scheduleNotificationAsync({ - content: { - title: notification.title, - body: notification.body, - data: { - type: notification.type, - ticketId: notification.ticketId, - tripId: notification.tripId, - ...notification.data, - }, - sound: this.settings.sound ? 'default' : undefined, - ...(Platform.OS === 'android' && { channelId }), - }, - trigger: trigger as Parameters[0]['trigger'], - }); - - return notificationId; - } - - /** - * Schedule trip reminder - */ async scheduleTripReminder( ticketId: number, tripId: number, @@ -290,148 +62,15 @@ class NotificationService { destination: string, departureTime: Date ): Promise { - if (!this.settings.tripReminders) { - return null; - } - - const reminderTime = new Date(departureTime); - reminderTime.setMinutes( - reminderTime.getMinutes() - this.settings.tripReminderMinutes - ); - - // Don't schedule if reminder time is in the past - if (reminderTime <= new Date()) { - return null; - } - - return this.scheduleLocalNotification( - { - type: 'trip_reminder', - ticketId, - tripId, - title: 'Trip Reminder', - body: `Your trip from ${origin} to ${destination} departs in ${this.settings.tripReminderMinutes} minutes`, - }, - { - date: reminderTime, - } - ); + return null; } - /** - * Send instant local notification - */ - async sendLocalNotification(notification: NotificationData): Promise { - await this.scheduleLocalNotification(notification, null); - } - - /** - * Cancel scheduled notification - */ async cancelNotification(notificationId: string): Promise { - if (isWeb || !Notifications) return; - await Notifications.cancelScheduledNotificationAsync(notificationId); + // No-op } - /** - * Cancel all scheduled notifications - */ - async cancelAllNotifications(): Promise { - if (isWeb || !Notifications) return; - await Notifications.cancelAllScheduledNotificationsAsync(); - } - - /** - * Get all scheduled notifications - */ - async getScheduledNotifications(): Promise { - if (isWeb || !Notifications) return []; - return Notifications.getAllScheduledNotificationsAsync(); - } - - /** - * Get badge count - */ - async getBadgeCount(): Promise { - if (isWeb || !Notifications) return 0; - return Notifications.getBadgeCountAsync(); - } - - /** - * Set badge count - */ - async setBadgeCount(count: number): Promise { - if (isWeb || !Notifications) return; - await Notifications.setBadgeCountAsync(count); - } - - /** - * Clear badge - */ async clearBadge(): Promise { - if (isWeb || !Notifications) return; - await Notifications.setBadgeCountAsync(0); - } - - /** - * Get notification settings - */ - getSettings(): NotificationSettings { - return { ...this.settings }; - } - - /** - * Update notification settings - */ - async updateSettings(newSettings: Partial): Promise { - this.settings = { ...this.settings, ...newSettings }; - await AsyncStorage.setItem( - NOTIFICATION_SETTINGS_KEY, - JSON.stringify(this.settings) - ); - - // Re-register if enabling notifications - if (newSettings.enabled && !this.pushToken) { - await this.registerForPushNotifications(); - } - } - - /** - * Load settings from storage - */ - private async loadSettings(): Promise { - try { - const stored = await AsyncStorage.getItem(NOTIFICATION_SETTINGS_KEY); - if (stored) { - this.settings = { ...DEFAULT_SETTINGS, ...JSON.parse(stored) }; - } - - // Load stored token - const storedToken = await AsyncStorage.getItem(PUSH_TOKEN_KEY); - if (storedToken) { - this.pushToken = storedToken; - } - } catch (error) { - console.error('Error loading notification settings:', error); - } - } - - /** - * Get Android channel for notification type - */ - private getChannelForType(type: NotificationData['type']): string { - switch (type) { - case 'trip_reminder': - return 'trip-reminders'; - case 'booking_confirmation': - return 'booking-confirmations'; - case 'trip_update': - return 'trip-updates'; - case 'promotion': - return 'promotions'; - default: - return 'general'; - } + // No-op } }