mirror of
https://github.com/odoobiznes/BUS-Ticket-client.git
synced 2026-05-28 06:24:44 +00:00
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 <noreply@anthropic.com>
This commit is contained in:
parent
87d9bda46a
commit
3aecbeb78f
15
app.json
15
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
|
||||
|
||||
182
package-lock.json
generated
182
package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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<SQLite.SQLiteDatabase> {
|
||||
if (!this.db) {
|
||||
this.db = await SQLite.openDatabaseAsync(DB_NAME);
|
||||
}
|
||||
return this.db;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize database schema
|
||||
*/
|
||||
async initialize(): Promise<void> {
|
||||
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<string | null> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
if (this.db) {
|
||||
await this.db.closeAsync();
|
||||
this.db = null;
|
||||
this.initialized = false;
|
||||
}
|
||||
// No-op for AsyncStorage
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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<NotificationSettings>) => Promise<void>;
|
||||
scheduleReminder: (
|
||||
@ -25,98 +24,10 @@ interface UseNotificationsReturn {
|
||||
}
|
||||
|
||||
export function useNotifications(): UseNotificationsReturn {
|
||||
const router = useRouter();
|
||||
const [expoPushToken, setExpoPushToken] = useState<string | null>(null);
|
||||
const [notification, setNotification] = useState<Notifications.Notification | null>(null);
|
||||
const [settings, setSettings] = useState<NotificationSettings>(
|
||||
notificationService.getSettings()
|
||||
);
|
||||
|
||||
const notificationListener = useRef<Notifications.Subscription>();
|
||||
const responseListener = useRef<Notifications.Subscription>();
|
||||
|
||||
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<NotificationSettings>) => {
|
||||
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<string | null> => {
|
||||
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,
|
||||
|
||||
@ -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<string, unknown>;
|
||||
@ -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<void> {
|
||||
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<string | null> {
|
||||
// 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<void> {
|
||||
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<NotificationSettings>): Promise<void> {
|
||||
this.settings = { ...this.settings, ...newSettings };
|
||||
}
|
||||
|
||||
/**
|
||||
* Register token with backend
|
||||
*/
|
||||
async registerTokenWithBackend(apiUrl: string, userId: number): Promise<void> {
|
||||
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<string> {
|
||||
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<typeof Notifications.scheduleNotificationAsync>[0]['trigger'],
|
||||
});
|
||||
|
||||
return notificationId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule trip reminder
|
||||
*/
|
||||
async scheduleTripReminder(
|
||||
ticketId: number,
|
||||
tripId: number,
|
||||
@ -290,148 +62,15 @@ class NotificationService {
|
||||
destination: string,
|
||||
departureTime: Date
|
||||
): Promise<string | null> {
|
||||
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<void> {
|
||||
await this.scheduleLocalNotification(notification, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel scheduled notification
|
||||
*/
|
||||
async cancelNotification(notificationId: string): Promise<void> {
|
||||
if (isWeb || !Notifications) return;
|
||||
await Notifications.cancelScheduledNotificationAsync(notificationId);
|
||||
// No-op
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel all scheduled notifications
|
||||
*/
|
||||
async cancelAllNotifications(): Promise<void> {
|
||||
if (isWeb || !Notifications) return;
|
||||
await Notifications.cancelAllScheduledNotificationsAsync();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all scheduled notifications
|
||||
*/
|
||||
async getScheduledNotifications(): Promise<unknown[]> {
|
||||
if (isWeb || !Notifications) return [];
|
||||
return Notifications.getAllScheduledNotificationsAsync();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get badge count
|
||||
*/
|
||||
async getBadgeCount(): Promise<number> {
|
||||
if (isWeb || !Notifications) return 0;
|
||||
return Notifications.getBadgeCountAsync();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set badge count
|
||||
*/
|
||||
async setBadgeCount(count: number): Promise<void> {
|
||||
if (isWeb || !Notifications) return;
|
||||
await Notifications.setBadgeCountAsync(count);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear badge
|
||||
*/
|
||||
async clearBadge(): Promise<void> {
|
||||
if (isWeb || !Notifications) return;
|
||||
await Notifications.setBadgeCountAsync(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get notification settings
|
||||
*/
|
||||
getSettings(): NotificationSettings {
|
||||
return { ...this.settings };
|
||||
}
|
||||
|
||||
/**
|
||||
* Update notification settings
|
||||
*/
|
||||
async updateSettings(newSettings: Partial<NotificationSettings>): Promise<void> {
|
||||
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<void> {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user