mirror of
https://github.com/odoobiznes/BUS-Ticket-client.git
synced 2026-05-28 07:24:46 +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": [
|
"plugins": [
|
||||||
"expo-router",
|
"expo-router",
|
||||||
"expo-secure-store",
|
"expo-secure-store",
|
||||||
"expo-localization",
|
"expo-localization"
|
||||||
[
|
|
||||||
"expo-local-authentication",
|
|
||||||
{
|
|
||||||
"faceIDPermission": "Allow BUS-Tickets to use Face ID for authentication"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"expo-notifications",
|
|
||||||
{
|
|
||||||
"icon": "./assets/notification-icon.png",
|
|
||||||
"color": "#e94560"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
],
|
],
|
||||||
"experiments": {
|
"experiments": {
|
||||||
"typedRoutes": true
|
"typedRoutes": true
|
||||||
|
|||||||
182
package-lock.json
generated
182
package-lock.json
generated
@ -25,14 +25,11 @@
|
|||||||
"expo-font": "~12.0.0",
|
"expo-font": "~12.0.0",
|
||||||
"expo-image": "~1.13.0",
|
"expo-image": "~1.13.0",
|
||||||
"expo-linking": "~6.3.0",
|
"expo-linking": "~6.3.0",
|
||||||
"expo-local-authentication": "~14.0.0",
|
|
||||||
"expo-localization": "~15.0.0",
|
"expo-localization": "~15.0.0",
|
||||||
"expo-network": "~6.0.0",
|
"expo-network": "~6.0.0",
|
||||||
"expo-notifications": "~0.28.0",
|
|
||||||
"expo-router": "~3.5.0",
|
"expo-router": "~3.5.0",
|
||||||
"expo-secure-store": "~13.0.0",
|
"expo-secure-store": "~13.0.0",
|
||||||
"expo-splash-screen": "~0.27.0",
|
"expo-splash-screen": "~0.27.0",
|
||||||
"expo-sqlite": "~14.0.0",
|
|
||||||
"expo-status-bar": "~1.12.0",
|
"expo-status-bar": "~1.12.0",
|
||||||
"expo-system-ui": "~3.0.7",
|
"expo-system-ui": "~3.0.7",
|
||||||
"expo-web-browser": "~13.0.0",
|
"expo-web-browser": "~13.0.0",
|
||||||
@ -3230,19 +3227,6 @@
|
|||||||
"react-native": "*"
|
"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": {
|
"node_modules/@expo/xcpretty": {
|
||||||
"version": "4.4.0",
|
"version": "4.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/@expo/xcpretty/-/xcpretty-4.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/@expo/xcpretty/-/xcpretty-4.4.0.tgz",
|
||||||
@ -3319,12 +3303,6 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "BSD-3-Clause"
|
"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": {
|
"node_modules/@isaacs/cliui": {
|
||||||
"version": "8.0.2",
|
"version": "8.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
||||||
@ -6949,12 +6927,6 @@
|
|||||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||||
"license": "Python-2.0"
|
"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": {
|
"node_modules/array-buffer-byte-length": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz",
|
"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==",
|
"integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/ast-types": {
|
||||||
"version": "0.15.2",
|
"version": "0.15.2",
|
||||||
"resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.15.2.tgz",
|
"resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.15.2.tgz",
|
||||||
@ -7464,12 +7423,6 @@
|
|||||||
"@babel/core": "^7.0.0"
|
"@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": {
|
"node_modules/balanced-match": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
|
||||||
@ -10062,18 +10015,6 @@
|
|||||||
"invariant": "^2.2.4"
|
"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": {
|
"node_modules/expo-localization": {
|
||||||
"version": "15.0.3",
|
"version": "15.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/expo-localization/-/expo-localization-15.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/expo-localization/-/expo-localization-15.0.3.tgz",
|
||||||
@ -10158,61 +10099,6 @@
|
|||||||
"expo": "*"
|
"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": {
|
"node_modules/expo-router": {
|
||||||
"version": "3.5.24",
|
"version": "3.5.24",
|
||||||
"resolved": "https://registry.npmjs.org/expo-router/-/expo-router-3.5.24.tgz",
|
"resolved": "https://registry.npmjs.org/expo-router/-/expo-router-3.5.24.tgz",
|
||||||
@ -10272,18 +10158,6 @@
|
|||||||
"expo": "*"
|
"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": {
|
"node_modules/expo-status-bar": {
|
||||||
"version": "1.12.1",
|
"version": "1.12.1",
|
||||||
"resolved": "https://registry.npmjs.org/expo-status-bar/-/expo-status-bar-1.12.1.tgz",
|
"resolved": "https://registry.npmjs.org/expo-status-bar/-/expo-status-bar-1.12.1.tgz",
|
||||||
@ -11493,12 +11367,6 @@
|
|||||||
"node": ">=16.x"
|
"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": {
|
"node_modules/import-fresh": {
|
||||||
"version": "3.3.1",
|
"version": "3.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
|
||||||
@ -11981,22 +11849,6 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"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": {
|
"node_modules/is-negative-zero": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz",
|
"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"
|
"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": {
|
"node_modules/normalize-path": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
||||||
@ -15443,22 +15289,6 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"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": {
|
"node_modules/object-keys": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
|
||||||
@ -16168,12 +15998,6 @@
|
|||||||
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
|
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/prelude-ls": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
||||||
@ -18796,12 +18620,6 @@
|
|||||||
"xtend": "~4.0.1"
|
"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": {
|
"node_modules/tinyglobby": {
|
||||||
"version": "0.2.15",
|
"version": "0.2.15",
|
||||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
|
||||||
|
|||||||
@ -41,14 +41,11 @@
|
|||||||
"expo-font": "~12.0.0",
|
"expo-font": "~12.0.0",
|
||||||
"expo-image": "~1.13.0",
|
"expo-image": "~1.13.0",
|
||||||
"expo-linking": "~6.3.0",
|
"expo-linking": "~6.3.0",
|
||||||
"expo-local-authentication": "~14.0.0",
|
|
||||||
"expo-localization": "~15.0.0",
|
"expo-localization": "~15.0.0",
|
||||||
"expo-network": "~6.0.0",
|
"expo-network": "~6.0.0",
|
||||||
"expo-notifications": "~0.28.0",
|
|
||||||
"expo-router": "~3.5.0",
|
"expo-router": "~3.5.0",
|
||||||
"expo-secure-store": "~13.0.0",
|
"expo-secure-store": "~13.0.0",
|
||||||
"expo-splash-screen": "~0.27.0",
|
"expo-splash-screen": "~0.27.0",
|
||||||
"expo-sqlite": "~14.0.0",
|
|
||||||
"expo-status-bar": "~1.12.0",
|
"expo-status-bar": "~1.12.0",
|
||||||
"expo-system-ui": "~3.0.7",
|
"expo-system-ui": "~3.0.7",
|
||||||
"expo-web-browser": "~13.0.0",
|
"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
|
* Copyright (c) 2024-2026 IT Enterprise
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as SQLite from 'expo-sqlite';
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
|
|
||||||
const DB_NAME = 'bus_tickets.db';
|
|
||||||
const DB_VERSION = 1;
|
|
||||||
|
|
||||||
class Database {
|
class Database {
|
||||||
private static instance: Database;
|
private static instance: Database;
|
||||||
private db: SQLite.SQLiteDatabase | null = null;
|
private initialized = false;
|
||||||
private initialized: boolean = false;
|
|
||||||
|
|
||||||
private constructor() {}
|
private constructor() {}
|
||||||
|
|
||||||
@ -22,161 +19,22 @@ class Database {
|
|||||||
return Database.instance;
|
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> {
|
async initialize(): Promise<void> {
|
||||||
if (this.initialized) return;
|
if (this.initialized) return;
|
||||||
|
console.log('Database initialized (AsyncStorage mode)');
|
||||||
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));
|
|
||||||
|
|
||||||
this.initialized = true;
|
this.initialized = true;
|
||||||
console.log('Database initialized');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get metadata value
|
|
||||||
*/
|
|
||||||
async getMetadata(key: string): Promise<string | null> {
|
async getMetadata(key: string): Promise<string | null> {
|
||||||
const db = await this.getDb();
|
return AsyncStorage.getItem(`@db_meta_${key}`);
|
||||||
const result = await db.getFirstAsync<{ value: string }>(
|
|
||||||
'SELECT value FROM metadata WHERE key = ?',
|
|
||||||
[key]
|
|
||||||
);
|
|
||||||
return result?.value || null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Set metadata value
|
|
||||||
*/
|
|
||||||
async setMetadata(key: string, value: string): Promise<void> {
|
async setMetadata(key: string, value: string): Promise<void> {
|
||||||
const db = await this.getDb();
|
await AsyncStorage.setItem(`@db_meta_${key}`, value);
|
||||||
await db.runAsync(
|
|
||||||
'INSERT OR REPLACE INTO metadata (key, value) VALUES (?, ?)',
|
|
||||||
[key, value]
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Close database connection
|
|
||||||
*/
|
|
||||||
async close(): Promise<void> {
|
async close(): Promise<void> {
|
||||||
if (this.db) {
|
// No-op for AsyncStorage
|
||||||
await this.db.closeAsync();
|
|
||||||
this.db = null;
|
|
||||||
this.initialized = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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
|
* Copyright (c) 2024-2026 IT Enterprise
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
import { useState, useCallback } from 'react';
|
||||||
import * as Notifications from 'expo-notifications';
|
|
||||||
import { useRouter } from 'expo-router';
|
|
||||||
import { notificationService, NotificationSettings } from '../services/NotificationService';
|
import { notificationService, NotificationSettings } from '../services/NotificationService';
|
||||||
|
|
||||||
interface UseNotificationsReturn {
|
interface UseNotificationsReturn {
|
||||||
expoPushToken: string | null;
|
expoPushToken: string | null;
|
||||||
notification: Notifications.Notification | null;
|
notification: null;
|
||||||
settings: NotificationSettings;
|
settings: NotificationSettings;
|
||||||
updateSettings: (settings: Partial<NotificationSettings>) => Promise<void>;
|
updateSettings: (settings: Partial<NotificationSettings>) => Promise<void>;
|
||||||
scheduleReminder: (
|
scheduleReminder: (
|
||||||
@ -25,98 +24,10 @@ interface UseNotificationsReturn {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function useNotifications(): 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>(
|
const [settings, setSettings] = useState<NotificationSettings>(
|
||||||
notificationService.getSettings()
|
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(
|
const updateSettings = useCallback(
|
||||||
async (newSettings: Partial<NotificationSettings>) => {
|
async (newSettings: Partial<NotificationSettings>) => {
|
||||||
await notificationService.updateSettings(newSettings);
|
await notificationService.updateSettings(newSettings);
|
||||||
@ -125,9 +36,6 @@ export function useNotifications(): UseNotificationsReturn {
|
|||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
|
||||||
* Schedule trip reminder
|
|
||||||
*/
|
|
||||||
const scheduleReminder = useCallback(
|
const scheduleReminder = useCallback(
|
||||||
async (
|
async (
|
||||||
ticketId: number,
|
ticketId: number,
|
||||||
@ -136,34 +44,22 @@ export function useNotifications(): UseNotificationsReturn {
|
|||||||
destination: string,
|
destination: string,
|
||||||
departureTime: Date
|
departureTime: Date
|
||||||
): Promise<string | null> => {
|
): Promise<string | null> => {
|
||||||
return notificationService.scheduleTripReminder(
|
return null;
|
||||||
ticketId,
|
|
||||||
tripId,
|
|
||||||
origin,
|
|
||||||
destination,
|
|
||||||
departureTime
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
|
||||||
* Cancel scheduled reminder
|
|
||||||
*/
|
|
||||||
const cancelReminder = useCallback(async (notificationId: string) => {
|
const cancelReminder = useCallback(async (notificationId: string) => {
|
||||||
await notificationService.cancelNotification(notificationId);
|
// No-op
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear badge count
|
|
||||||
*/
|
|
||||||
const clearBadge = useCallback(async () => {
|
const clearBadge = useCallback(async () => {
|
||||||
await notificationService.clearBadge();
|
// No-op
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
expoPushToken,
|
expoPushToken: null,
|
||||||
notification,
|
notification: null,
|
||||||
settings,
|
settings,
|
||||||
updateSettings,
|
updateSettings,
|
||||||
scheduleReminder,
|
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
|
* 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 {
|
export interface NotificationSettings {
|
||||||
enabled: boolean;
|
|
||||||
tripReminders: boolean;
|
tripReminders: boolean;
|
||||||
tripReminderMinutes: number; // Minutes before departure
|
|
||||||
bookingConfirmations: boolean;
|
|
||||||
promotions: boolean;
|
promotions: boolean;
|
||||||
tripUpdates: boolean;
|
bookingUpdates: boolean;
|
||||||
sound: boolean;
|
reminderTime: number;
|
||||||
vibration: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
export interface NotificationData {
|
||||||
type: 'trip_reminder' | 'booking_confirmation' | 'trip_update' | 'promotion' | 'general';
|
|
||||||
ticketId?: number;
|
|
||||||
tripId?: number;
|
|
||||||
title: string;
|
title: string;
|
||||||
body: string;
|
body: string;
|
||||||
data?: Record<string, unknown>;
|
data?: Record<string, unknown>;
|
||||||
@ -56,9 +19,12 @@ export interface NotificationData {
|
|||||||
|
|
||||||
class NotificationService {
|
class NotificationService {
|
||||||
private static instance: NotificationService;
|
private static instance: NotificationService;
|
||||||
private pushToken: string | null = null;
|
private settings: NotificationSettings = {
|
||||||
private settings: NotificationSettings = DEFAULT_SETTINGS;
|
tripReminders: true,
|
||||||
private initialized: boolean = false;
|
promotions: false,
|
||||||
|
bookingUpdates: true,
|
||||||
|
reminderTime: 60,
|
||||||
|
};
|
||||||
|
|
||||||
private constructor() {}
|
private constructor() {}
|
||||||
|
|
||||||
@ -69,220 +35,26 @@ class NotificationService {
|
|||||||
return NotificationService.instance;
|
return NotificationService.instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize notification service
|
|
||||||
*/
|
|
||||||
async initialize(): Promise<void> {
|
async initialize(): Promise<void> {
|
||||||
if (this.initialized) return;
|
console.log('Notifications disabled in this build');
|
||||||
|
|
||||||
// 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 {
|
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> {
|
async registerTokenWithBackend(apiUrl: string, userId: number): Promise<void> {
|
||||||
if (!this.pushToken) {
|
// No-op
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 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(
|
async scheduleTripReminder(
|
||||||
ticketId: number,
|
ticketId: number,
|
||||||
tripId: number,
|
tripId: number,
|
||||||
@ -290,148 +62,15 @@ class NotificationService {
|
|||||||
destination: string,
|
destination: string,
|
||||||
departureTime: Date
|
departureTime: Date
|
||||||
): Promise<string | null> {
|
): Promise<string | null> {
|
||||||
if (!this.settings.tripReminders) {
|
return null;
|
||||||
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,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Send instant local notification
|
|
||||||
*/
|
|
||||||
async sendLocalNotification(notification: NotificationData): Promise<void> {
|
|
||||||
await this.scheduleLocalNotification(notification, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cancel scheduled notification
|
|
||||||
*/
|
|
||||||
async cancelNotification(notificationId: string): Promise<void> {
|
async cancelNotification(notificationId: string): Promise<void> {
|
||||||
if (isWeb || !Notifications) return;
|
// No-op
|
||||||
await Notifications.cancelScheduledNotificationAsync(notificationId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 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> {
|
async clearBadge(): Promise<void> {
|
||||||
if (isWeb || !Notifications) return;
|
// No-op
|
||||||
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';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user