auth: replace password login with OIDC PKCE flow (Phase 5)
- Install expo-auth-session + expo-web-browser - Add 'bincio-rec' URL scheme to app.json for deep-link redirect - auth.ts: generate PKCE verifier/challenge, open bincio.org/oauth2/authorize in browser, exchange auth code for RS256 id_token, store in AsyncStorage - SettingsScreen: remove handle/password fields, single 'Sign in with bincio' button that opens the browser flow
This commit is contained in:
@@ -2,6 +2,7 @@
|
|||||||
"expo": {
|
"expo": {
|
||||||
"name": "bincio-rec",
|
"name": "bincio-rec",
|
||||||
"slug": "bincio-rec",
|
"slug": "bincio-rec",
|
||||||
|
"scheme": "bincio-rec",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"orientation": "portrait",
|
"orientation": "portrait",
|
||||||
"icon": "./assets/icon.png",
|
"icon": "./assets/icon.png",
|
||||||
@@ -12,7 +13,9 @@
|
|||||||
"infoPlist": {
|
"infoPlist": {
|
||||||
"NSLocationWhenInUseUsageDescription": "bincio-rec uses your location to record your activity track.",
|
"NSLocationWhenInUseUsageDescription": "bincio-rec uses your location to record your activity track.",
|
||||||
"NSLocationAlwaysAndWhenInUseUsageDescription": "bincio-rec uses your location in the background to record your activity track.",
|
"NSLocationAlwaysAndWhenInUseUsageDescription": "bincio-rec uses your location in the background to record your activity track.",
|
||||||
"UIBackgroundModes": ["location"]
|
"UIBackgroundModes": [
|
||||||
|
"location"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"android": {
|
"android": {
|
||||||
@@ -39,7 +42,9 @@
|
|||||||
"web": {
|
"web": {
|
||||||
"favicon": "./assets/favicon.png"
|
"favicon": "./assets/favicon.png"
|
||||||
},
|
},
|
||||||
"ignorePaths": ["CLAUDE.md"],
|
"ignorePaths": [
|
||||||
|
"CLAUDE.md"
|
||||||
|
],
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"expo-sqlite",
|
"expo-sqlite",
|
||||||
[
|
[
|
||||||
@@ -56,7 +61,8 @@
|
|||||||
"icon": "./assets/icon.png",
|
"icon": "./assets/icon.png",
|
||||||
"color": "#3b82f6"
|
"color": "#3b82f6"
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"expo-web-browser"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+207
-14
@@ -14,9 +14,10 @@
|
|||||||
"@react-navigation/native": "^7.2.5",
|
"@react-navigation/native": "^7.2.5",
|
||||||
"@react-navigation/native-stack": "^7.16.0",
|
"@react-navigation/native-stack": "^7.16.0",
|
||||||
"expo": "~56.0.8",
|
"expo": "~56.0.8",
|
||||||
"expo-av": "^16.0.8",
|
"expo-auth-session": "~56.0.13",
|
||||||
"expo-crypto": "^56.0.4",
|
"expo-crypto": "^56.0.4",
|
||||||
"expo-file-system": "~56.0.7",
|
"expo-file-system": "~56.0.7",
|
||||||
|
"expo-intent-launcher": "~56.0.4",
|
||||||
"expo-keep-awake": "~56.0.3",
|
"expo-keep-awake": "~56.0.3",
|
||||||
"expo-location": "~56.0.15",
|
"expo-location": "~56.0.15",
|
||||||
"expo-notifications": "~56.0.15",
|
"expo-notifications": "~56.0.15",
|
||||||
@@ -24,12 +25,14 @@
|
|||||||
"expo-sqlite": "~56.0.4",
|
"expo-sqlite": "~56.0.4",
|
||||||
"expo-status-bar": "~56.0.4",
|
"expo-status-bar": "~56.0.4",
|
||||||
"expo-task-manager": "~56.0.16",
|
"expo-task-manager": "~56.0.16",
|
||||||
|
"expo-web-browser": "~56.0.5",
|
||||||
"react": "19.2.3",
|
"react": "19.2.3",
|
||||||
"react-native": "0.85.3",
|
"react-native": "0.85.3",
|
||||||
"react-native-ble-plx": "^3.5.1",
|
"react-native-ble-plx": "^3.5.1",
|
||||||
"react-native-gesture-handler": "~2.31.1",
|
"react-native-gesture-handler": "~2.31.1",
|
||||||
"react-native-safe-area-context": "~5.7.0",
|
"react-native-safe-area-context": "~5.7.0",
|
||||||
"react-native-screens": "4.25.2",
|
"react-native-screens": "4.25.2",
|
||||||
|
"react-native-svg": "15.15.4",
|
||||||
"zustand": "^5.0.14"
|
"zustand": "^5.0.14"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -2441,6 +2444,12 @@
|
|||||||
"node": ">=0.6"
|
"node": ">=0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/boolbase": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/bplist-creator": {
|
"node_modules/bplist-creator": {
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.1.0.tgz",
|
||||||
@@ -2842,6 +2851,56 @@
|
|||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/css-select": {
|
||||||
|
"version": "5.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz",
|
||||||
|
"integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"boolbase": "^1.0.0",
|
||||||
|
"css-what": "^6.1.0",
|
||||||
|
"domhandler": "^5.0.2",
|
||||||
|
"domutils": "^3.0.1",
|
||||||
|
"nth-check": "^2.0.1"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/fb55"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/css-tree": {
|
||||||
|
"version": "1.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz",
|
||||||
|
"integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mdn-data": "2.0.14",
|
||||||
|
"source-map": "^0.6.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/css-tree/node_modules/source-map": {
|
||||||
|
"version": "0.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||||
|
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/css-what": {
|
||||||
|
"version": "6.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz",
|
||||||
|
"integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/fb55"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/csstype": {
|
"node_modules/csstype": {
|
||||||
"version": "3.2.3",
|
"version": "3.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||||
@@ -2929,6 +2988,61 @@
|
|||||||
"integrity": "sha512-AmGyK9WpNf06WeP5TjHZq/wNzP76OuEeaiTlKr9E/EEelYLczywUKoqRz+DPRq/ErssjT4lU+/W7wzJW+7K/ZA==",
|
"integrity": "sha512-AmGyK9WpNf06WeP5TjHZq/wNzP76OuEeaiTlKr9E/EEelYLczywUKoqRz+DPRq/ErssjT4lU+/W7wzJW+7K/ZA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/dom-serializer": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"domelementtype": "^2.3.0",
|
||||||
|
"domhandler": "^5.0.2",
|
||||||
|
"entities": "^4.2.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/domelementtype": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/fb55"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "BSD-2-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/domhandler": {
|
||||||
|
"version": "5.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
|
||||||
|
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"domelementtype": "^2.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/domhandler?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/domutils": {
|
||||||
|
"version": "3.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
|
||||||
|
"integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"dom-serializer": "^2.0.0",
|
||||||
|
"domelementtype": "^2.3.0",
|
||||||
|
"domhandler": "^5.0.3"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/domutils?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ee-first": {
|
"node_modules/ee-first": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||||
@@ -2956,6 +3070,18 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/entities": {
|
||||||
|
"version": "4.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||||
|
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/error-stack-parser": {
|
"node_modules/error-stack-parser": {
|
||||||
"version": "2.1.4",
|
"version": "2.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz",
|
||||||
@@ -3090,21 +3216,22 @@
|
|||||||
"expo": "*"
|
"expo": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/expo-av": {
|
"node_modules/expo-auth-session": {
|
||||||
"version": "16.0.8",
|
"version": "56.0.13",
|
||||||
"resolved": "https://registry.npmjs.org/expo-av/-/expo-av-16.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/expo-auth-session/-/expo-auth-session-56.0.13.tgz",
|
||||||
"integrity": "sha512-cmVPftGR/ca7XBgs7R6ky36lF3OC0/MM/lpgX/yXqfv0jASTsh7AYX9JxHCwFmF+Z6JEB1vne9FDx4GiLcGreQ==",
|
"integrity": "sha512-LR8Suq8BHKRFBUcAKTMmZufCcDcr0sQa8rIYit1r7kshrqAy9glIUU4aqHt8tflW/ISN0x1vU+HU8AQaackM0A==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peerDependencies": {
|
"dependencies": {
|
||||||
"expo": "*",
|
"expo-application": "~56.0.3",
|
||||||
"react": "*",
|
"expo-constants": "~56.0.16",
|
||||||
"react-native": "*",
|
"expo-crypto": "~56.0.4",
|
||||||
"react-native-web": "*"
|
"expo-linking": "~56.0.13",
|
||||||
|
"expo-web-browser": "~56.0.5",
|
||||||
|
"invariant": "^2.2.4"
|
||||||
},
|
},
|
||||||
"peerDependenciesMeta": {
|
"peerDependencies": {
|
||||||
"react-native-web": {
|
"react": "*",
|
||||||
"optional": true
|
"react-native": "*"
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/expo-constants": {
|
"node_modules/expo-constants": {
|
||||||
@@ -3139,6 +3266,15 @@
|
|||||||
"react-native": "*"
|
"react-native": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/expo-intent-launcher": {
|
||||||
|
"version": "56.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/expo-intent-launcher/-/expo-intent-launcher-56.0.4.tgz",
|
||||||
|
"integrity": "sha512-ZqRMuPunNSucK9kRbtX+I8bD/OhY0moGNnHKHCWx//1BZ+HyKBCVw6OX12gWHjowz1AhcSVeAasYt48Y+tXPwQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"expo": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/expo-keep-awake": {
|
"node_modules/expo-keep-awake": {
|
||||||
"version": "56.0.3",
|
"version": "56.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/expo-keep-awake/-/expo-keep-awake-56.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/expo-keep-awake/-/expo-keep-awake-56.0.3.tgz",
|
||||||
@@ -3149,6 +3285,20 @@
|
|||||||
"react": "*"
|
"react": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/expo-linking": {
|
||||||
|
"version": "56.0.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/expo-linking/-/expo-linking-56.0.13.tgz",
|
||||||
|
"integrity": "sha512-38YrpTh6xdiDxmYSDIUffDqev1hIcEggw2fZ3IZhNp2DVLF1xvqsbO6hJD1fuBKN8P34B3Ggc9Yy26fkqdfCOA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"expo-constants": "~56.0.16",
|
||||||
|
"invariant": "^2.2.4"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "*",
|
||||||
|
"react-native": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/expo-location": {
|
"node_modules/expo-location": {
|
||||||
"version": "56.0.15",
|
"version": "56.0.15",
|
||||||
"resolved": "https://registry.npmjs.org/expo-location/-/expo-location-56.0.15.tgz",
|
"resolved": "https://registry.npmjs.org/expo-location/-/expo-location-56.0.15.tgz",
|
||||||
@@ -3287,6 +3437,16 @@
|
|||||||
"react-native": "*"
|
"react-native": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/expo-web-browser": {
|
||||||
|
"version": "56.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/expo-web-browser/-/expo-web-browser-56.0.5.tgz",
|
||||||
|
"integrity": "sha512-kaN+wcR5lHwPCH1IgrU1XyPUQvBRzdF1TMp65uAF9iUCyipqYnmrvV87eqAmrdkFFopWVgU7FcxPu1UZw+gvUQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"expo": "*",
|
||||||
|
"react-native": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/expo/node_modules/@expo/cli": {
|
"node_modules/expo/node_modules/@expo/cli": {
|
||||||
"version": "56.1.13",
|
"version": "56.1.13",
|
||||||
"resolved": "https://registry.npmjs.org/@expo/cli/-/cli-56.1.13.tgz",
|
"resolved": "https://registry.npmjs.org/@expo/cli/-/cli-56.1.13.tgz",
|
||||||
@@ -4681,6 +4841,12 @@
|
|||||||
"integrity": "sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==",
|
"integrity": "sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==",
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
|
"node_modules/mdn-data": {
|
||||||
|
"version": "2.0.14",
|
||||||
|
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz",
|
||||||
|
"integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==",
|
||||||
|
"license": "CC0-1.0"
|
||||||
|
},
|
||||||
"node_modules/memoize-one": {
|
"node_modules/memoize-one": {
|
||||||
"version": "5.2.1",
|
"version": "5.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
|
||||||
@@ -5189,6 +5355,18 @@
|
|||||||
"node": "^16.14.0 || >=18.0.0"
|
"node": "^16.14.0 || >=18.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/nth-check": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"boolbase": "^1.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/nth-check?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/nullthrows": {
|
"node_modules/nullthrows": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz",
|
||||||
@@ -5761,6 +5939,21 @@
|
|||||||
"react-native": ">=0.82.0"
|
"react-native": ">=0.82.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-native-svg": {
|
||||||
|
"version": "15.15.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-15.15.4.tgz",
|
||||||
|
"integrity": "sha512-boT/vIRgj6zZKBpfTPJJiYWMbZE9duBMOwPK6kCSTgxsS947IFMOq9OgIFkpWZTB7t229H24pDRkh3W9ZK/J1A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"css-select": "^5.1.0",
|
||||||
|
"css-tree": "^1.1.3",
|
||||||
|
"warn-once": "0.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "*",
|
||||||
|
"react-native": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-native/node_modules/commander": {
|
"node_modules/react-native/node_modules/commander": {
|
||||||
"version": "12.1.0",
|
"version": "12.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
|
||||||
|
|||||||
+4
-1
@@ -9,9 +9,10 @@
|
|||||||
"@react-navigation/native": "^7.2.5",
|
"@react-navigation/native": "^7.2.5",
|
||||||
"@react-navigation/native-stack": "^7.16.0",
|
"@react-navigation/native-stack": "^7.16.0",
|
||||||
"expo": "~56.0.8",
|
"expo": "~56.0.8",
|
||||||
"expo-av": "^16.0.8",
|
"expo-auth-session": "~56.0.13",
|
||||||
"expo-crypto": "^56.0.4",
|
"expo-crypto": "^56.0.4",
|
||||||
"expo-file-system": "~56.0.7",
|
"expo-file-system": "~56.0.7",
|
||||||
|
"expo-intent-launcher": "~56.0.4",
|
||||||
"expo-keep-awake": "~56.0.3",
|
"expo-keep-awake": "~56.0.3",
|
||||||
"expo-location": "~56.0.15",
|
"expo-location": "~56.0.15",
|
||||||
"expo-notifications": "~56.0.15",
|
"expo-notifications": "~56.0.15",
|
||||||
@@ -19,12 +20,14 @@
|
|||||||
"expo-sqlite": "~56.0.4",
|
"expo-sqlite": "~56.0.4",
|
||||||
"expo-status-bar": "~56.0.4",
|
"expo-status-bar": "~56.0.4",
|
||||||
"expo-task-manager": "~56.0.16",
|
"expo-task-manager": "~56.0.16",
|
||||||
|
"expo-web-browser": "~56.0.5",
|
||||||
"react": "19.2.3",
|
"react": "19.2.3",
|
||||||
"react-native": "0.85.3",
|
"react-native": "0.85.3",
|
||||||
"react-native-ble-plx": "^3.5.1",
|
"react-native-ble-plx": "^3.5.1",
|
||||||
"react-native-gesture-handler": "~2.31.1",
|
"react-native-gesture-handler": "~2.31.1",
|
||||||
"react-native-safe-area-context": "~5.7.0",
|
"react-native-safe-area-context": "~5.7.0",
|
||||||
"react-native-screens": "4.25.2",
|
"react-native-screens": "4.25.2",
|
||||||
|
"react-native-svg": "15.15.4",
|
||||||
"zustand": "^5.0.14"
|
"zustand": "^5.0.14"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
View, Text, TextInput, StyleSheet, Switch,
|
View, Text, StyleSheet, Switch,
|
||||||
TouchableOpacity, Alert, ScrollView, ActivityIndicator,
|
TouchableOpacity, Alert, ScrollView, ActivityIndicator,
|
||||||
} from 'react-native';
|
} from 'react-native';
|
||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
@@ -131,34 +131,28 @@ function AppTab() {
|
|||||||
|
|
||||||
function SyncTab() {
|
function SyncTab() {
|
||||||
const { accent } = useTheme();
|
const { accent } = useTheme();
|
||||||
const [instanceUrl, setInstanceUrl] = useState('');
|
|
||||||
const [handle, setHandle] = useState('');
|
|
||||||
const [password, setPassword] = useState('');
|
|
||||||
const [connectedAs, setConnectedAs] = useState<string | null>(null);
|
const [connectedAs, setConnectedAs] = useState<string | null>(null);
|
||||||
const [connecting, setConnecting] = useState(false);
|
const [connecting, setConnecting] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadAuthState().then((auth) => {
|
loadAuthState().then((auth) => {
|
||||||
if (auth) { setInstanceUrl(auth.instanceUrl); setHandle(auth.handle); setConnectedAs(auth.handle); }
|
if (auth) setConnectedAs(auth.handle);
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
async function handleConnect() {
|
async function handleConnect() {
|
||||||
if (!instanceUrl.trim()) { Alert.alert('Required', 'Enter the instance URL.'); return; }
|
|
||||||
if (!handle.trim()) { Alert.alert('Required', 'Enter your handle.'); return; }
|
|
||||||
if (!password) { Alert.alert('Required', 'Enter your password.'); return; }
|
|
||||||
setConnecting(true);
|
setConnecting(true);
|
||||||
const result = await login(instanceUrl.trim(), handle.trim(), password);
|
const result = await login();
|
||||||
setConnecting(false);
|
setConnecting(false);
|
||||||
if (result.ok) { setConnectedAs(result.displayName || handle.trim()); setPassword(''); }
|
if (result.ok) setConnectedAs(result.displayName ?? '');
|
||||||
else Alert.alert('Login failed', result.error ?? 'Unknown error');
|
else if (result.error !== 'Cancelled') Alert.alert('Sign in failed', result.error ?? 'Unknown error');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleDisconnect() {
|
async function handleDisconnect() {
|
||||||
Alert.alert('Disconnect', 'Remove saved credentials?', [
|
Alert.alert('Disconnect', 'Remove saved credentials?', [
|
||||||
{ text: 'Cancel', style: 'cancel' },
|
{ text: 'Cancel', style: 'cancel' },
|
||||||
{ text: 'Disconnect', style: 'destructive', onPress: async () => {
|
{ text: 'Disconnect', style: 'destructive', onPress: async () => {
|
||||||
await logout(); setConnectedAs(null); setHandle(''); setPassword('');
|
await logout(); setConnectedAs(null);
|
||||||
}},
|
}},
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@@ -167,18 +161,6 @@ function SyncTab() {
|
|||||||
<ScrollView contentContainerStyle={styles.content}>
|
<ScrollView contentContainerStyle={styles.content}>
|
||||||
<Text style={styles.sectionTitle}>bincio instance</Text>
|
<Text style={styles.sectionTitle}>bincio instance</Text>
|
||||||
|
|
||||||
<Text style={styles.label}>Instance URL</Text>
|
|
||||||
<TextInput
|
|
||||||
style={styles.input}
|
|
||||||
value={instanceUrl}
|
|
||||||
onChangeText={setInstanceUrl}
|
|
||||||
placeholder="https://bincio.example.com"
|
|
||||||
placeholderTextColor={colors.placeholder}
|
|
||||||
autoCapitalize="none"
|
|
||||||
keyboardType="url"
|
|
||||||
editable={!connectedAs}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{connectedAs ? (
|
{connectedAs ? (
|
||||||
<View style={styles.connectedBox}>
|
<View style={styles.connectedBox}>
|
||||||
<View>
|
<View>
|
||||||
@@ -191,16 +173,9 @@ function SyncTab() {
|
|||||||
</View>
|
</View>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Text style={styles.label}>Handle</Text>
|
<Text style={styles.hint}>Sign in with your bincio account to sync recordings.</Text>
|
||||||
<TextInput style={styles.input} value={handle} onChangeText={setHandle} placeholder="your-handle" placeholderTextColor={colors.placeholder} autoCapitalize="none" autoCorrect={false} />
|
|
||||||
|
|
||||||
<Text style={styles.label}>Password</Text>
|
|
||||||
<TextInput style={styles.input} value={password} onChangeText={setPassword} placeholder="••••••••" placeholderTextColor={colors.placeholder} secureTextEntry />
|
|
||||||
|
|
||||||
<Text style={styles.hint}>Your password is used once to obtain a session token, then forgotten.</Text>
|
|
||||||
|
|
||||||
<TouchableOpacity style={[styles.connectBtn, connecting && styles.connectBtnDisabled]} onPress={handleConnect} disabled={connecting}>
|
<TouchableOpacity style={[styles.connectBtn, connecting && styles.connectBtnDisabled]} onPress={handleConnect} disabled={connecting}>
|
||||||
{connecting ? <ActivityIndicator color={colors.text} /> : <Text style={styles.connectBtnText}>Connect</Text>}
|
{connecting ? <ActivityIndicator color={colors.text} /> : <Text style={styles.connectBtnText}>Sign in with bincio</Text>}
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
+76
-29
@@ -1,42 +1,89 @@
|
|||||||
import AsyncStorage from '@react-native-async-storage/async-storage';
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
||||||
|
import * as Crypto from 'expo-crypto';
|
||||||
|
import * as WebBrowser from 'expo-web-browser';
|
||||||
|
import { makeRedirectUri } from 'expo-auth-session';
|
||||||
|
|
||||||
interface LoginResult {
|
const ISSUER = 'https://bincio.org';
|
||||||
|
const CLIENT_ID = 'bincio-rec';
|
||||||
|
const REDIRECT_URI = makeRedirectUri({ scheme: 'bincio-rec', path: 'oauth' });
|
||||||
|
|
||||||
|
// ── PKCE helpers ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async function generateVerifier(): Promise<string> {
|
||||||
|
const bytes = await Crypto.getRandomBytesAsync(32);
|
||||||
|
return btoa(String.fromCharCode(...bytes))
|
||||||
|
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deriveChallenge(verifier: string): Promise<string> {
|
||||||
|
const digest = await Crypto.digestStringAsync(
|
||||||
|
Crypto.CryptoDigestAlgorithm.SHA256,
|
||||||
|
verifier,
|
||||||
|
{ encoding: Crypto.CryptoEncoding.BASE64 },
|
||||||
|
);
|
||||||
|
return digest.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Public API ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export interface LoginResult {
|
||||||
ok: boolean;
|
ok: boolean;
|
||||||
displayName?: string;
|
displayName?: string;
|
||||||
error?: string;
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function login(
|
export async function login(): Promise<LoginResult> {
|
||||||
instanceUrl: string,
|
const verifier = await generateVerifier();
|
||||||
handle: string,
|
const challenge = await deriveChallenge(verifier);
|
||||||
password: string,
|
|
||||||
): Promise<LoginResult> {
|
|
||||||
const url = instanceUrl.replace(/\/$/, '') + '/api/auth/token';
|
|
||||||
try {
|
|
||||||
const resp = await fetch(url, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ handle, password }),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!resp.ok) {
|
const authUrl = `${ISSUER}/oauth2/authorize?` + new URLSearchParams({
|
||||||
const text = await resp.text().catch(() => '');
|
client_id: CLIENT_ID,
|
||||||
return { ok: false, error: text || `HTTP ${resp.status}` };
|
redirect_uri: REDIRECT_URI,
|
||||||
}
|
response_type: 'code',
|
||||||
|
scope: 'openid profile',
|
||||||
|
code_challenge: challenge,
|
||||||
|
code_challenge_method: 'S256',
|
||||||
|
});
|
||||||
|
|
||||||
const data = await resp.json();
|
const result = await WebBrowser.openAuthSessionAsync(authUrl, REDIRECT_URI);
|
||||||
if (!data.token) return { ok: false, error: 'No token in response' };
|
if (result.type !== 'success') {
|
||||||
|
return { ok: false, error: result.type === 'cancel' ? 'Cancelled' : 'Browser error' };
|
||||||
await Promise.all([
|
|
||||||
AsyncStorage.setItem('instanceUrl', instanceUrl.trim()),
|
|
||||||
AsyncStorage.setItem('handle', handle.trim()),
|
|
||||||
AsyncStorage.setItem('apiToken', data.token),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return { ok: true, displayName: data.display_name };
|
|
||||||
} catch (e: any) {
|
|
||||||
return { ok: false, error: e?.message ?? 'Connection failed' };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const code = new URL(result.url).searchParams.get('code');
|
||||||
|
if (!code) return { ok: false, error: 'No code in redirect' };
|
||||||
|
|
||||||
|
const tokenResp = await fetch(`${ISSUER}/oauth2/token`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
|
body: new URLSearchParams({
|
||||||
|
grant_type: 'authorization_code',
|
||||||
|
code,
|
||||||
|
redirect_uri: REDIRECT_URI,
|
||||||
|
client_id: CLIENT_ID,
|
||||||
|
code_verifier: verifier,
|
||||||
|
}).toString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!tokenResp.ok) {
|
||||||
|
const text = await tokenResp.text().catch(() => '');
|
||||||
|
return { ok: false, error: `Token exchange failed: ${text}` };
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokenData = await tokenResp.json();
|
||||||
|
const idToken: string = tokenData.id_token;
|
||||||
|
if (!idToken) return { ok: false, error: 'No id_token in response' };
|
||||||
|
|
||||||
|
// Decode payload (no verification needed — server already validated)
|
||||||
|
const payload = JSON.parse(atob(idToken.split('.')[1].replace(/-/g, '+').replace(/_/g, '/')));
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
AsyncStorage.setItem('instanceUrl', ISSUER),
|
||||||
|
AsyncStorage.setItem('handle', payload.sub ?? ''),
|
||||||
|
AsyncStorage.setItem('apiToken', idToken),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return { ok: true, displayName: payload.name ?? payload.preferred_username };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function logout(): Promise<void> {
|
export async function logout(): Promise<void> {
|
||||||
|
|||||||
Reference in New Issue
Block a user