Compare commits
10 commits
defenseur-
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b9f45be8df | ||
| 96adf00495 | |||
|
|
98cd423a50 | ||
| 5aec3b5271 | |||
|
|
0e4c5cdb4e | ||
| 2da7c0d67d | |||
|
|
9eaa103a87 | ||
|
|
d4c77e640c | ||
| a45860e5fd | |||
|
|
9e0adaf5c9 |
12 changed files with 816 additions and 169 deletions
3
STATE.md
3
STATE.md
|
|
@ -1,6 +1,6 @@
|
|||
# STATE — simpl-liste
|
||||
|
||||
> Derniere MAJ : 2026-05-30 (deploy web liste.lacompagnie + #90)
|
||||
> Derniere MAJ : 2026-07-27 (par fix-issue #68)
|
||||
|
||||
## Position actuelle
|
||||
|
||||
|
|
@ -15,6 +15,7 @@ Coolify malgre la spec). Procedure en memoire `simpl-liste-web-deploy`.
|
|||
|
||||
## Decisions recentes
|
||||
|
||||
- 2026-07-27 : #68 merged (PR #101) — Feedback Hub integre (bouton Reglages > A propos, POST feedback.lacompagniemaximus.com app_id simpl-liste, opt-in contexte + identite Loi 25 ; client sans dependance teste via node:test).
|
||||
- 2026-05-30 : #90 merged (PR #93, ThemeToggle eslint-disable) + premier deploy web depuis 7 sem (rsync source/ + docker compose --build sur VPS) — #70 et #90 live sur liste.lacompagniemaximus.com.
|
||||
- 2026-05-30 : PR #89 merged — display name web resolu via userInfo (ordre fallback de la vitrine), fix #70.
|
||||
- 2026-05-30 : nettoyage lint web/ (PR #91, 2 prefer-const + import inutilise) ; #90 analyse (ThemeToggle set-state-in-effect, decision : eslint-disable documente).
|
||||
|
|
|
|||
4
app.json
4
app.json
|
|
@ -2,7 +2,7 @@
|
|||
"expo": {
|
||||
"name": "Simpl-Liste",
|
||||
"slug": "simpl-liste",
|
||||
"version": "1.6.5",
|
||||
"version": "1.6.6",
|
||||
"orientation": "portrait",
|
||||
"icon": "./assets/images/icon.png",
|
||||
"scheme": "simplliste",
|
||||
|
|
@ -24,7 +24,7 @@
|
|||
"backgroundColor": "#FFF8F0"
|
||||
},
|
||||
"edgeToEdgeEnabled": true,
|
||||
"versionCode": 17
|
||||
"versionCode": 18
|
||||
},
|
||||
"plugins": [
|
||||
"expo-router",
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { useState, useEffect, useCallback } from 'react';
|
|||
import { View, Text, Pressable, useColorScheme, TextInput, ScrollView, Alert, Modal, Platform, Switch, Linking, ActivityIndicator } from 'react-native';
|
||||
import { KeyboardAvoidingView } from 'react-native-keyboard-controller';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Sun, Moon, Smartphone, Plus, Trash2, Pencil, Bell, CalendarDays, LayoutGrid, Mail, RefreshCw, Cloud, LogIn, LogOut } from 'lucide-react-native';
|
||||
import { Sun, Moon, Smartphone, Plus, Trash2, Pencil, Bell, CalendarDays, LayoutGrid, Mail, RefreshCw, Cloud, LogIn, LogOut, MessageSquarePlus } from 'lucide-react-native';
|
||||
import Constants from 'expo-constants';
|
||||
|
||||
import { useLogto } from '@logto/rn';
|
||||
|
|
@ -14,6 +14,7 @@ import { initCalendar } from '@/src/services/calendar';
|
|||
import { syncWidgetData } from '@/src/services/widgetSync';
|
||||
import { fullSync, initialMerge, initialReset } from '@/src/services/syncClient';
|
||||
import i18n from '@/src/i18n';
|
||||
import FeedbackModal from '@/src/components/FeedbackModal';
|
||||
|
||||
type ThemeMode = 'light' | 'dark' | 'system';
|
||||
|
||||
|
|
@ -40,6 +41,7 @@ export default function SettingsScreen() {
|
|||
const [tagName, setTagName] = useState('');
|
||||
const [tagColor, setTagColor] = useState(TAG_COLORS[0]);
|
||||
const [checkingUpdate, setCheckingUpdate] = useState(false);
|
||||
const [showFeedback, setShowFeedback] = useState(false);
|
||||
const [isSyncing, setIsSyncing] = useState(false);
|
||||
const { signIn: logtoSignIn, signOut: logtoSignOut, getIdTokenClaims, isAuthenticated } = useLogto();
|
||||
|
||||
|
|
@ -694,6 +696,12 @@ export default function SettingsScreen() {
|
|||
</KeyboardAvoidingView>
|
||||
</Modal>
|
||||
|
||||
<FeedbackModal
|
||||
visible={showFeedback}
|
||||
onClose={() => setShowFeedback(false)}
|
||||
isDark={isDark}
|
||||
/>
|
||||
|
||||
{/* About Section */}
|
||||
<View className="px-4 pb-8 pt-6">
|
||||
<Text
|
||||
|
|
@ -728,6 +736,18 @@ export default function SettingsScreen() {
|
|||
{t('settings.checkUpdate')}
|
||||
</Text>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
onPress={() => setShowFeedback(true)}
|
||||
className={`flex-row items-center border-b px-4 py-3.5 ${isDark ? 'border-[#3A3A3A]' : 'border-[#E5E7EB]'}`}
|
||||
>
|
||||
<MessageSquarePlus size={20} color={isDark ? '#A0A0A0' : '#6B6B6B'} />
|
||||
<Text
|
||||
className={`ml-3 text-base ${isDark ? 'text-[#F5F5F5]' : 'text-[#1A1A1A]'}`}
|
||||
style={{ fontFamily: 'Inter_400Regular' }}
|
||||
>
|
||||
{t('feedback.open')}
|
||||
</Text>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
onPress={() => Linking.openURL('mailto:lacompagniemaximus@protonmail.com')}
|
||||
className="flex-row items-center px-4 py-3.5"
|
||||
|
|
|
|||
306
package-lock.json
generated
306
package-lock.json
generated
|
|
@ -1,18 +1,18 @@
|
|||
{
|
||||
"name": "simpl-liste",
|
||||
"version": "1.6.5",
|
||||
"version": "1.6.6",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "simpl-liste",
|
||||
"version": "1.6.5",
|
||||
"version": "1.6.6",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@expo-google-fonts/inter": "^0.4.2",
|
||||
"@expo/ngrok": "^4.1.3",
|
||||
"@expo/vector-icons": "^15.0.3",
|
||||
"@logto/rn": "^1.2.0",
|
||||
"@logto/rn": "^1.1.0",
|
||||
"@react-native-async-storage/async-storage": "2.2.0",
|
||||
"@react-native-community/datetimepicker": "8.4.4",
|
||||
"@react-navigation/native": "^7.1.8",
|
||||
|
|
@ -92,12 +92,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
"version": "7.29.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
|
||||
"integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz",
|
||||
"integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-validator-identifier": "^7.28.5",
|
||||
"@babel/helper-validator-identifier": "^7.29.7",
|
||||
"js-tokens": "^4.0.0",
|
||||
"picocolors": "^1.1.1"
|
||||
},
|
||||
|
|
@ -106,29 +106,29 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@babel/compat-data": {
|
||||
"version": "7.29.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz",
|
||||
"integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz",
|
||||
"integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/core": {
|
||||
"version": "7.29.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
|
||||
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz",
|
||||
"integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.29.0",
|
||||
"@babel/generator": "^7.29.0",
|
||||
"@babel/helper-compilation-targets": "^7.28.6",
|
||||
"@babel/helper-module-transforms": "^7.28.6",
|
||||
"@babel/helpers": "^7.28.6",
|
||||
"@babel/parser": "^7.29.0",
|
||||
"@babel/template": "^7.28.6",
|
||||
"@babel/traverse": "^7.29.0",
|
||||
"@babel/types": "^7.29.0",
|
||||
"@babel/code-frame": "^7.29.7",
|
||||
"@babel/generator": "^7.29.7",
|
||||
"@babel/helper-compilation-targets": "^7.29.7",
|
||||
"@babel/helper-module-transforms": "^7.29.7",
|
||||
"@babel/helpers": "^7.29.7",
|
||||
"@babel/parser": "^7.29.7",
|
||||
"@babel/template": "^7.29.7",
|
||||
"@babel/traverse": "^7.29.7",
|
||||
"@babel/types": "^7.29.7",
|
||||
"@jridgewell/remapping": "^2.3.5",
|
||||
"convert-source-map": "^2.0.0",
|
||||
"debug": "^4.1.0",
|
||||
|
|
@ -145,13 +145,13 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@babel/generator": {
|
||||
"version": "7.29.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz",
|
||||
"integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==",
|
||||
"version": "7.29.8",
|
||||
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz",
|
||||
"integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.29.0",
|
||||
"@babel/types": "^7.29.0",
|
||||
"@babel/parser": "^7.29.8",
|
||||
"@babel/types": "^7.29.8",
|
||||
"@jridgewell/gen-mapping": "^0.3.12",
|
||||
"@jridgewell/trace-mapping": "^0.3.28",
|
||||
"jsesc": "^3.0.2"
|
||||
|
|
@ -173,13 +173,13 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@babel/helper-compilation-targets": {
|
||||
"version": "7.28.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz",
|
||||
"integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz",
|
||||
"integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/compat-data": "^7.28.6",
|
||||
"@babel/helper-validator-option": "^7.27.1",
|
||||
"@babel/compat-data": "^7.29.7",
|
||||
"@babel/helper-validator-option": "^7.29.7",
|
||||
"browserslist": "^4.24.0",
|
||||
"lru-cache": "^5.1.1",
|
||||
"semver": "^6.3.1"
|
||||
|
|
@ -243,9 +243,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@babel/helper-globals": {
|
||||
"version": "7.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
|
||||
"integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz",
|
||||
"integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
|
|
@ -265,27 +265,27 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@babel/helper-module-imports": {
|
||||
"version": "7.28.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
|
||||
"integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz",
|
||||
"integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/traverse": "^7.28.6",
|
||||
"@babel/types": "^7.28.6"
|
||||
"@babel/traverse": "^7.29.7",
|
||||
"@babel/types": "^7.29.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-module-transforms": {
|
||||
"version": "7.28.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz",
|
||||
"integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz",
|
||||
"integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-module-imports": "^7.28.6",
|
||||
"@babel/helper-validator-identifier": "^7.28.5",
|
||||
"@babel/traverse": "^7.28.6"
|
||||
"@babel/helper-module-imports": "^7.29.7",
|
||||
"@babel/helper-validator-identifier": "^7.29.7",
|
||||
"@babel/traverse": "^7.29.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
|
|
@ -363,27 +363,27 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@babel/helper-string-parser": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
|
||||
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
|
||||
"integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-validator-identifier": {
|
||||
"version": "7.28.5",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
|
||||
"integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
|
||||
"integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-validator-option": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
|
||||
"integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz",
|
||||
"integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
|
|
@ -404,13 +404,13 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@babel/helpers": {
|
||||
"version": "7.28.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz",
|
||||
"integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz",
|
||||
"integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/template": "^7.28.6",
|
||||
"@babel/types": "^7.28.6"
|
||||
"@babel/template": "^7.29.7",
|
||||
"@babel/types": "^7.29.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
|
|
@ -503,12 +503,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@babel/parser": {
|
||||
"version": "7.29.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz",
|
||||
"integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==",
|
||||
"version": "7.29.8",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz",
|
||||
"integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.29.0"
|
||||
"@babel/types": "^7.29.8"
|
||||
},
|
||||
"bin": {
|
||||
"parser": "bin/babel-parser.js"
|
||||
|
|
@ -1505,31 +1505,31 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@babel/template": {
|
||||
"version": "7.28.6",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
|
||||
"integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz",
|
||||
"integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.28.6",
|
||||
"@babel/parser": "^7.28.6",
|
||||
"@babel/types": "^7.28.6"
|
||||
"@babel/code-frame": "^7.29.7",
|
||||
"@babel/parser": "^7.29.7",
|
||||
"@babel/types": "^7.29.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/traverse": {
|
||||
"version": "7.29.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz",
|
||||
"integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==",
|
||||
"version": "7.29.8",
|
||||
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz",
|
||||
"integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.29.0",
|
||||
"@babel/generator": "^7.29.0",
|
||||
"@babel/helper-globals": "^7.28.0",
|
||||
"@babel/parser": "^7.29.0",
|
||||
"@babel/template": "^7.28.6",
|
||||
"@babel/types": "^7.29.0",
|
||||
"@babel/code-frame": "^7.29.7",
|
||||
"@babel/generator": "^7.29.8",
|
||||
"@babel/helper-globals": "^7.29.7",
|
||||
"@babel/parser": "^7.29.8",
|
||||
"@babel/template": "^7.29.7",
|
||||
"@babel/types": "^7.29.8",
|
||||
"debug": "^4.3.1"
|
||||
},
|
||||
"engines": {
|
||||
|
|
@ -1556,13 +1556,13 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@babel/types": {
|
||||
"version": "7.29.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
|
||||
"integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
|
||||
"version": "7.29.8",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz",
|
||||
"integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-string-parser": "^7.27.1",
|
||||
"@babel/helper-validator-identifier": "^7.28.5"
|
||||
"@babel/helper-string-parser": "^7.29.7",
|
||||
"@babel/helper-validator-identifier": "^7.29.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
|
|
@ -2679,34 +2679,6 @@
|
|||
"excpretty": "build/cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/@expo/xcpretty/node_modules/argparse": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||
"license": "Python-2.0"
|
||||
},
|
||||
"node_modules/@expo/xcpretty/node_modules/js-yaml": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz",
|
||||
"integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/puzrin"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/nodeca"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"argparse": "^2.0.1"
|
||||
},
|
||||
"bin": {
|
||||
"js-yaml": "bin/js-yaml.js"
|
||||
}
|
||||
},
|
||||
"node_modules/@ide/backoff": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@ide/backoff/-/backoff-1.0.0.tgz",
|
||||
|
|
@ -2760,9 +2732,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": {
|
||||
"version": "3.15.0",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.0.tgz",
|
||||
"integrity": "sha512-ttBQIIQPDeLjpPOohtUdXuXUVoA2uIB6fEH9HyJ7234s5mBJ5wTx20njxplLZQgLaOfpmPQA7X2t5AX6tIPbog==",
|
||||
"version": "3.15.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.1.tgz",
|
||||
"integrity": "sha512-S99WuO3HlhO3XN41EtYUNl9zzXjoJx7QvmipxsJVxtCBT0YHEFy+iOJhjSvrmV12nYhWpZaM8lPHkJm0yUMbag==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"argparse": "^1.0.7",
|
||||
|
|
@ -2958,9 +2930,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@logto/rn": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@logto/rn/-/rn-1.2.0.tgz",
|
||||
"integrity": "sha512-sQLlkQ+h78JLHVxraYy6ehPSb++pym1rn5jRhwiLKbctUd/CZNBub/XUWJqRwX6JYtOux6VDZ0e5wNe/5EW++w==",
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@logto/rn/-/rn-1.1.0.tgz",
|
||||
"integrity": "sha512-GcB6gGrjBASrTy4FsyJWCgYHaCjl2Tl/6CL+OZfU9Vro7meyfrW2+bHBOi7aKeXl+tLNqUybHoFcv+sVvUObxw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@logto/client": "3.1.2",
|
||||
|
|
@ -2970,9 +2942,9 @@
|
|||
},
|
||||
"peerDependencies": {
|
||||
"@react-native-async-storage/async-storage": ">=1.23.1 <3",
|
||||
"expo-crypto": ">=14.0.2 <57",
|
||||
"expo-secure-store": ">=14.0.1 <57",
|
||||
"expo-web-browser": ">=14.0.2 <57",
|
||||
"expo-crypto": ">=14.0.2 <16",
|
||||
"expo-secure-store": ">=14.0.1 <16",
|
||||
"expo-web-browser": ">=14.0.2 <16",
|
||||
"react-native": ">=0.76.0 <1"
|
||||
}
|
||||
},
|
||||
|
|
@ -3336,9 +3308,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@react-native/codegen/node_modules/brace-expansion": {
|
||||
"version": "1.1.13",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
|
||||
"integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
|
||||
"version": "1.1.18",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz",
|
||||
"integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
|
|
@ -4386,15 +4358,15 @@
|
|||
}
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "5.0.6",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
|
||||
"integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
|
||||
"version": "5.0.9",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz",
|
||||
"integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^4.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
"node": "20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/brace-expansion/node_modules/balanced-match": {
|
||||
|
|
@ -6439,9 +6411,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/expo/node_modules/brace-expansion": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.0.tgz",
|
||||
"integrity": "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==",
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz",
|
||||
"integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
|
|
@ -7745,6 +7717,34 @@
|
|||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
"version": "4.3.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz",
|
||||
"integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/puzrin"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/nodeca"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"argparse": "^2.0.1"
|
||||
},
|
||||
"bin": {
|
||||
"js-yaml": "bin/js-yaml.js"
|
||||
}
|
||||
},
|
||||
"node_modules/js-yaml/node_modules/argparse": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||
"license": "Python-2.0"
|
||||
},
|
||||
"node_modules/jsc-safe-url": {
|
||||
"version": "0.2.4",
|
||||
"resolved": "https://registry.npmjs.org/jsc-safe-url/-/jsc-safe-url-0.2.4.tgz",
|
||||
|
|
@ -8844,9 +8844,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.11",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
||||
"version": "3.3.18",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz",
|
||||
"integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
|
|
@ -9519,9 +9519,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.12",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.12.tgz",
|
||||
"integrity": "sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==",
|
||||
"version": "8.5.26",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz",
|
||||
"integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
|
|
@ -9538,7 +9538,7 @@
|
|||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"nanoid": "^3.3.17",
|
||||
"picocolors": "^1.1.1",
|
||||
"source-map-js": "^1.2.1"
|
||||
},
|
||||
|
|
@ -10533,9 +10533,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/react-native/node_modules/brace-expansion": {
|
||||
"version": "1.1.13",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
|
||||
"integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
|
||||
"version": "1.1.18",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz",
|
||||
"integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
|
|
@ -10953,9 +10953,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/rimraf/node_modules/brace-expansion": {
|
||||
"version": "1.1.13",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
|
||||
"integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
|
||||
"version": "1.1.18",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz",
|
||||
"integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
|
|
@ -11591,9 +11591,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/tar": {
|
||||
"version": "7.5.19",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.19.tgz",
|
||||
"integrity": "sha512-4LeEWl96twnS2Q7Bz4MGqgazLqO+hJN63GZxXoIqh1T3VweYD997gbU1ItNsQafqqXTXd5WFyFdReLtwvRBNiw==",
|
||||
"version": "7.5.22",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.22.tgz",
|
||||
"integrity": "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"@isaacs/fs-minipass": "^4.0.0",
|
||||
|
|
@ -11679,9 +11679,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/test-exclude/node_modules/brace-expansion": {
|
||||
"version": "1.1.13",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
|
||||
"integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
|
||||
"version": "1.1.18",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz",
|
||||
"integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
|
|
@ -11907,9 +11907,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/undici": {
|
||||
"version": "6.27.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.27.0.tgz",
|
||||
"integrity": "sha512-YmfV3YnEDzXRC5lZ2jWtWWHKGUm1zIt8AhesR1tens+HTNv+YZlN/dp6G727LOvMJ8xjP9Be7Y2Sdr96LDm+pg==",
|
||||
"version": "6.28.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.28.0.tgz",
|
||||
"integrity": "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.17"
|
||||
|
|
|
|||
24
package.json
24
package.json
|
|
@ -1,10 +1,10 @@
|
|||
{
|
||||
"name": "simpl-liste",
|
||||
"main": "index.js",
|
||||
"version": "1.6.5",
|
||||
"version": "1.6.6",
|
||||
"scripts": {
|
||||
"start": "expo start",
|
||||
"test": "node tests/smoke.test.cjs",
|
||||
"test": "node tests/smoke.test.cjs && node --test --disable-warning=ExperimentalWarning --disable-warning=MODULE_TYPELESS_PACKAGE_JSON tests/feedback.test.mjs",
|
||||
"android": "expo start --android",
|
||||
"ios": "expo start --ios",
|
||||
"web": "expo start --web",
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
"@expo-google-fonts/inter": "^0.4.2",
|
||||
"@expo/ngrok": "^4.1.3",
|
||||
"@expo/vector-icons": "^15.0.3",
|
||||
"@logto/rn": "^1.2.0",
|
||||
"@logto/rn": "^1.1.0",
|
||||
"@react-native-async-storage/async-storage": "2.2.0",
|
||||
"@react-native-community/datetimepicker": "8.4.4",
|
||||
"@react-navigation/native": "^7.1.8",
|
||||
|
|
@ -70,11 +70,17 @@
|
|||
"esbuild": "^0.25.0",
|
||||
"@xmldom/xmldom": "^0.8.13",
|
||||
"uuid": "^11.1.1",
|
||||
"postcss": "^8.5.10",
|
||||
"postcss": "^8.5.18",
|
||||
"js-yaml@^3.0.0": "^3.15.1",
|
||||
"js-yaml@^4.0.0": "^4.3.1",
|
||||
"brace-expansion@^1.0.0": "^1.1.18",
|
||||
"brace-expansion@^2.0.0": "^2.1.4",
|
||||
"brace-expansion@^5.0.0": "^5.0.9",
|
||||
"@babel/core": "^7.29.6",
|
||||
"@expo/cli": {
|
||||
"ws": "^8.20.1",
|
||||
"undici": "^6.27.0",
|
||||
"tar": "^7.5.19"
|
||||
"undici": "^6.28.0",
|
||||
"tar": "^7.5.21"
|
||||
},
|
||||
"react-devtools-core": {
|
||||
"shell-quote": "^1.8.4",
|
||||
|
|
@ -88,12 +94,6 @@
|
|||
},
|
||||
"metro": {
|
||||
"ws": "^7.5.11"
|
||||
},
|
||||
"@expo/xcpretty": {
|
||||
"js-yaml": "^4.2.0"
|
||||
},
|
||||
"@istanbuljs/load-nyc-config": {
|
||||
"js-yaml": "^3.15.0"
|
||||
}
|
||||
},
|
||||
"private": true
|
||||
|
|
|
|||
253
src/components/FeedbackModal.tsx
Normal file
253
src/components/FeedbackModal.tsx
Normal file
|
|
@ -0,0 +1,253 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
import {
|
||||
View,
|
||||
Text,
|
||||
Pressable,
|
||||
Modal,
|
||||
TextInput,
|
||||
ActivityIndicator,
|
||||
Dimensions,
|
||||
Platform,
|
||||
} from 'react-native';
|
||||
import { KeyboardAvoidingView } from 'react-native-keyboard-controller';
|
||||
import { usePathname } from 'expo-router';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { X, MessageSquarePlus, CheckCircle, AlertCircle, Check } from 'lucide-react-native';
|
||||
import Constants from 'expo-constants';
|
||||
|
||||
import { colors } from '@/src/theme/colors';
|
||||
import { useSettingsStore } from '@/src/stores/useSettingsStore';
|
||||
import { useFeedback } from '@/src/hooks/useFeedback';
|
||||
import { MAX_CONTENT_LENGTH, type FeedbackContext } from '@/src/services/feedback';
|
||||
|
||||
interface FeedbackModalProps {
|
||||
visible: boolean;
|
||||
onClose: () => void;
|
||||
isDark: boolean;
|
||||
}
|
||||
|
||||
const AUTO_CLOSE_MS = 2000;
|
||||
|
||||
export default function FeedbackModal({ visible, onClose, isDark }: FeedbackModalProps) {
|
||||
const { t, i18n } = useTranslation();
|
||||
const pathname = usePathname();
|
||||
const { userId } = useSettingsStore();
|
||||
const { state, submit, reset } = useFeedback();
|
||||
|
||||
const [content, setContent] = useState('');
|
||||
const [includeContext, setIncludeContext] = useState(false);
|
||||
const [identify, setIdentify] = useState(false);
|
||||
|
||||
const trimmed = content.trim();
|
||||
const isSending = state.status === 'sending';
|
||||
const isSuccess = state.status === 'success';
|
||||
const canSubmit = trimmed.length > 0 && !isSending && !isSuccess;
|
||||
|
||||
const textColor = isDark ? '#F5F5F5' : '#1A1A1A';
|
||||
const mutedColor = isDark ? '#A0A0A0' : '#6B6B6B';
|
||||
const borderColor = isDark ? '#3A3A3A' : '#E5E7EB';
|
||||
const successColor = colors.priority.low;
|
||||
const errorColor = colors.terracotta.DEFAULT;
|
||||
|
||||
// Reset both local and hook state each time the modal opens.
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
setContent('');
|
||||
setIncludeContext(false);
|
||||
setIdentify(false);
|
||||
reset();
|
||||
}
|
||||
}, [visible, reset]);
|
||||
|
||||
// Auto-close after a successful send; timer cleared on unmount / state change.
|
||||
useEffect(() => {
|
||||
if (!isSuccess) return;
|
||||
const timer = setTimeout(() => {
|
||||
onClose();
|
||||
reset();
|
||||
}, AUTO_CLOSE_MS);
|
||||
return () => clearTimeout(timer);
|
||||
}, [isSuccess, onClose, reset]);
|
||||
|
||||
const handleClose = () => {
|
||||
if (isSending) return;
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!canSubmit) return;
|
||||
|
||||
let context: FeedbackContext | undefined;
|
||||
if (includeContext) {
|
||||
const { width, height } = Dimensions.get('window');
|
||||
context = {
|
||||
page: pathname,
|
||||
locale: i18n.language,
|
||||
theme: isDark ? 'dark' : 'light',
|
||||
viewport: `${Math.round(width)}x${Math.round(height)}`,
|
||||
userAgent: `Simpl-Liste/${Constants.expoConfig?.version ?? '0.0.0'} (${Platform.OS})`,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
const feedbackUserId = identify && userId ? userId : null;
|
||||
await submit({ content, userId: feedbackUserId, context });
|
||||
};
|
||||
|
||||
const errorMessage = (() => {
|
||||
if (state.status !== 'error' || !state.errorCode) return null;
|
||||
switch (state.errorCode) {
|
||||
case 'rate_limit':
|
||||
return t('feedback.error.rateLimit');
|
||||
case 'invalid':
|
||||
return t('feedback.error.invalid');
|
||||
default:
|
||||
return t('feedback.error.generic');
|
||||
}
|
||||
})();
|
||||
|
||||
return (
|
||||
<Modal visible={visible} transparent animationType="fade" onRequestClose={handleClose}>
|
||||
<KeyboardAvoidingView behavior="padding" style={{ flex: 1 }}>
|
||||
<Pressable onPress={handleClose} className="flex-1 justify-end bg-black/40">
|
||||
<Pressable
|
||||
onPress={(e) => e.stopPropagation()}
|
||||
className={`rounded-t-2xl px-4 pb-8 pt-4 ${isDark ? 'bg-[#2A2A2A]' : 'bg-white'}`}
|
||||
>
|
||||
{/* Header */}
|
||||
<View className="mb-4 flex-row items-center justify-between">
|
||||
<View className="flex-row items-center">
|
||||
<MessageSquarePlus size={20} color={colors.bleu.DEFAULT} />
|
||||
<Text className="ml-2 text-lg" style={{ fontFamily: 'Inter_600SemiBold', color: textColor }}>
|
||||
{t('feedback.title')}
|
||||
</Text>
|
||||
</View>
|
||||
<Pressable onPress={handleClose} disabled={isSending} className="p-1">
|
||||
<X size={20} color={mutedColor} />
|
||||
</Pressable>
|
||||
</View>
|
||||
|
||||
{isSuccess ? (
|
||||
<View className="items-center justify-center py-10">
|
||||
<CheckCircle size={40} color={successColor} />
|
||||
<Text
|
||||
className="mt-3 text-base"
|
||||
style={{ fontFamily: 'Inter_500Medium', color: textColor }}
|
||||
>
|
||||
{t('feedback.success')}
|
||||
</Text>
|
||||
</View>
|
||||
) : (
|
||||
<>
|
||||
<TextInput
|
||||
value={content}
|
||||
onChangeText={(v) => setContent(v.slice(0, MAX_CONTENT_LENGTH))}
|
||||
placeholder={t('feedback.placeholder')}
|
||||
placeholderTextColor={mutedColor}
|
||||
editable={!isSending}
|
||||
multiline
|
||||
className="min-h-[120px] rounded-xl border px-3 py-2.5 text-base"
|
||||
style={{
|
||||
fontFamily: 'Inter_400Regular',
|
||||
color: textColor,
|
||||
borderColor,
|
||||
textAlignVertical: 'top',
|
||||
}}
|
||||
/>
|
||||
<Text className="mt-1 self-end text-xs" style={{ color: mutedColor }}>
|
||||
{content.length}/{MAX_CONTENT_LENGTH}
|
||||
</Text>
|
||||
|
||||
{/* Opt-in: navigation context */}
|
||||
<Pressable
|
||||
onPress={() => setIncludeContext((v) => !v)}
|
||||
disabled={isSending}
|
||||
className="mt-3 flex-row items-center"
|
||||
>
|
||||
<View
|
||||
className="h-5 w-5 items-center justify-center rounded border"
|
||||
style={{
|
||||
borderColor: includeContext ? colors.bleu.DEFAULT : borderColor,
|
||||
backgroundColor: includeContext ? colors.bleu.DEFAULT : 'transparent',
|
||||
}}
|
||||
>
|
||||
{includeContext && <Check size={14} color="#FFFFFF" />}
|
||||
</View>
|
||||
<Text
|
||||
className="ml-2 flex-1 text-sm"
|
||||
style={{ fontFamily: 'Inter_400Regular', color: textColor }}
|
||||
>
|
||||
{t('feedback.includeContext')}
|
||||
</Text>
|
||||
</Pressable>
|
||||
|
||||
{/* Opt-in: identify (only when signed in) */}
|
||||
{!!userId && (
|
||||
<Pressable
|
||||
onPress={() => setIdentify((v) => !v)}
|
||||
disabled={isSending}
|
||||
className="mt-2 flex-row items-center"
|
||||
>
|
||||
<View
|
||||
className="h-5 w-5 items-center justify-center rounded border"
|
||||
style={{
|
||||
borderColor: identify ? colors.bleu.DEFAULT : borderColor,
|
||||
backgroundColor: identify ? colors.bleu.DEFAULT : 'transparent',
|
||||
}}
|
||||
>
|
||||
{identify && <Check size={14} color="#FFFFFF" />}
|
||||
</View>
|
||||
<Text
|
||||
className="ml-2 flex-1 text-sm"
|
||||
style={{ fontFamily: 'Inter_400Regular', color: textColor }}
|
||||
>
|
||||
{t('feedback.identify')}
|
||||
</Text>
|
||||
</Pressable>
|
||||
)}
|
||||
|
||||
{errorMessage && (
|
||||
<View className="mt-3 flex-row items-center">
|
||||
<AlertCircle size={16} color={errorColor} />
|
||||
<Text
|
||||
className="ml-2 flex-1 text-sm"
|
||||
style={{ fontFamily: 'Inter_400Regular', color: errorColor }}
|
||||
>
|
||||
{errorMessage}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Actions */}
|
||||
<View className="mt-5 flex-row items-center justify-end">
|
||||
<Pressable onPress={handleClose} disabled={isSending} className="mr-2 px-4 py-2.5">
|
||||
<Text
|
||||
className="text-sm"
|
||||
style={{ fontFamily: 'Inter_500Medium', color: mutedColor }}
|
||||
>
|
||||
{t('feedback.cancel')}
|
||||
</Text>
|
||||
</Pressable>
|
||||
<Pressable
|
||||
onPress={handleSubmit}
|
||||
disabled={!canSubmit}
|
||||
className="rounded-lg bg-bleu px-4 py-2.5"
|
||||
style={{ opacity: canSubmit ? 1 : 0.5 }}
|
||||
>
|
||||
{isSending ? (
|
||||
<ActivityIndicator size={18} color="#FFFFFF" />
|
||||
) : (
|
||||
<Text className="text-sm text-white" style={{ fontFamily: 'Inter_600SemiBold' }}>
|
||||
{t('feedback.submit')}
|
||||
</Text>
|
||||
)}
|
||||
</Pressable>
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
</Pressable>
|
||||
</Pressable>
|
||||
</KeyboardAvoidingView>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
55
src/hooks/useFeedback.ts
Normal file
55
src/hooks/useFeedback.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import { useCallback, useReducer } from 'react';
|
||||
|
||||
import {
|
||||
sendFeedback,
|
||||
type FeedbackErrorCode,
|
||||
type SendFeedbackInput,
|
||||
} from '@/src/services/feedback';
|
||||
|
||||
export type FeedbackStatus = 'idle' | 'sending' | 'success' | 'error';
|
||||
|
||||
export interface FeedbackState {
|
||||
status: FeedbackStatus;
|
||||
errorCode: FeedbackErrorCode | null;
|
||||
}
|
||||
|
||||
type FeedbackAction =
|
||||
| { type: 'SEND_START' }
|
||||
| { type: 'SEND_SUCCESS' }
|
||||
| { type: 'SEND_ERROR'; code: FeedbackErrorCode }
|
||||
| { type: 'RESET' };
|
||||
|
||||
const initialState: FeedbackState = { status: 'idle', errorCode: null };
|
||||
|
||||
function reducer(_state: FeedbackState, action: FeedbackAction): FeedbackState {
|
||||
switch (action.type) {
|
||||
case 'SEND_START':
|
||||
return { status: 'sending', errorCode: null };
|
||||
case 'SEND_SUCCESS':
|
||||
return { status: 'success', errorCode: null };
|
||||
case 'SEND_ERROR':
|
||||
return { status: 'error', errorCode: action.code };
|
||||
case 'RESET':
|
||||
return initialState;
|
||||
}
|
||||
}
|
||||
|
||||
// Feedback submission state machine (idle -> sending -> success | error).
|
||||
// The network call never throws; the outcome is derived from its result.
|
||||
export function useFeedback() {
|
||||
const [state, dispatch] = useReducer(reducer, initialState);
|
||||
|
||||
const submit = useCallback(async (args: SendFeedbackInput) => {
|
||||
dispatch({ type: 'SEND_START' });
|
||||
const result = await sendFeedback(args);
|
||||
if (result.ok) {
|
||||
dispatch({ type: 'SEND_SUCCESS' });
|
||||
} else {
|
||||
dispatch({ type: 'SEND_ERROR', code: result.code });
|
||||
}
|
||||
}, []);
|
||||
|
||||
const reset = useCallback(() => dispatch({ type: 'RESET' }), []);
|
||||
|
||||
return { state, submit, reset };
|
||||
}
|
||||
|
|
@ -105,6 +105,21 @@
|
|||
"download": "Download",
|
||||
"updateError": "Unable to check for updates"
|
||||
},
|
||||
"feedback": {
|
||||
"open": "Send feedback",
|
||||
"title": "Your feedback",
|
||||
"placeholder": "Describe your suggestion or the issue you ran into...",
|
||||
"cancel": "Cancel",
|
||||
"submit": "Send",
|
||||
"includeContext": "Include navigation context",
|
||||
"identify": "Identify me (attach my account)",
|
||||
"success": "Thanks for your feedback!",
|
||||
"error": {
|
||||
"rateLimit": "Too many submissions. Try again in a moment.",
|
||||
"invalid": "Invalid message. Check the content.",
|
||||
"generic": "Couldn't send. Try again later."
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
"title": "Notifications",
|
||||
"enabled": "Reminders enabled",
|
||||
|
|
|
|||
|
|
@ -105,6 +105,21 @@
|
|||
"download": "Télécharger",
|
||||
"updateError": "Impossible de vérifier les mises à jour"
|
||||
},
|
||||
"feedback": {
|
||||
"open": "Envoyer un feedback",
|
||||
"title": "Votre feedback",
|
||||
"placeholder": "Décrivez votre suggestion ou le problème rencontré...",
|
||||
"cancel": "Annuler",
|
||||
"submit": "Envoyer",
|
||||
"includeContext": "Inclure le contexte de navigation",
|
||||
"identify": "M'identifier (joindre mon compte)",
|
||||
"success": "Merci pour votre feedback !",
|
||||
"error": {
|
||||
"rateLimit": "Trop d'envois. Réessayez dans un moment.",
|
||||
"invalid": "Message invalide. Vérifiez le contenu.",
|
||||
"generic": "Envoi impossible. Réessayez plus tard."
|
||||
}
|
||||
},
|
||||
"notifications": {
|
||||
"title": "Notifications",
|
||||
"enabled": "Rappels activés",
|
||||
|
|
|
|||
100
src/services/feedback.ts
Normal file
100
src/services/feedback.ts
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
// Feedback Hub client — posts user feedback to the centralized micro-service
|
||||
// (feedback.lacompagniemaximus.com). The endpoint is public (no auth) and
|
||||
// re-sanitizes everything server-side; the mirror logic here keeps the payload
|
||||
// minimal and predictable.
|
||||
//
|
||||
// IMPORTANT: this module is deliberately dependency-free (no RN/Expo imports,
|
||||
// no `@/` path alias, only the global `fetch`/`console`). Its pure helpers are
|
||||
// unit-tested with `node --test` via native TS type-stripping
|
||||
// (tests/feedback.test.mjs), which cannot resolve the `@/` alias. Keep it
|
||||
// import-free so that test path stays valid — the smoke test enforces this.
|
||||
|
||||
export const FEEDBACK_HUB_URL = 'https://feedback.lacompagniemaximus.com';
|
||||
export const APP_ID = 'simpl-liste';
|
||||
export const MAX_CONTENT_LENGTH = 2000;
|
||||
const MAX_CONTEXT_VALUE_LENGTH = 500;
|
||||
|
||||
// Strict whitelist mirrored from the server's `sanitizeContext`. Any other key
|
||||
// is dropped server-side anyway; we drop it here too.
|
||||
export const CONTEXT_KEYS = [
|
||||
'page',
|
||||
'locale',
|
||||
'theme',
|
||||
'viewport',
|
||||
'userAgent',
|
||||
'timestamp',
|
||||
] as const;
|
||||
|
||||
export type FeedbackContextKey = (typeof CONTEXT_KEYS)[number];
|
||||
export type FeedbackContext = Partial<Record<FeedbackContextKey, string>>;
|
||||
|
||||
export type FeedbackErrorCode =
|
||||
| 'invalid'
|
||||
| 'rate_limit'
|
||||
| 'server_error'
|
||||
| 'network_error';
|
||||
|
||||
export type FeedbackResult = { ok: true } | { ok: false; code: FeedbackErrorCode };
|
||||
|
||||
export interface SendFeedbackInput {
|
||||
content: string;
|
||||
userId?: string | null;
|
||||
context?: FeedbackContext;
|
||||
}
|
||||
|
||||
// Client-side cap on the raw text the user typed. Note: the server strips HTML
|
||||
// *then* caps at 2000, so markup-heavy input may be shortened server-side. The
|
||||
// server is authoritative; here we cap the raw string so the char counter and
|
||||
// payload stay bounded.
|
||||
export function capContent(content: string): string {
|
||||
return content.trim().slice(0, MAX_CONTENT_LENGTH);
|
||||
}
|
||||
|
||||
// Keep only whitelisted string keys, each truncated to 500 chars. Returns
|
||||
// undefined when nothing survives so the field is omitted from the payload.
|
||||
export function sanitizeContext(
|
||||
raw: FeedbackContext | null | undefined,
|
||||
): FeedbackContext | undefined {
|
||||
if (!raw) return undefined;
|
||||
const out: FeedbackContext = {};
|
||||
for (const key of CONTEXT_KEYS) {
|
||||
const value = raw[key];
|
||||
if (typeof value === 'string' && value.length > 0) {
|
||||
out[key] = value.slice(0, MAX_CONTEXT_VALUE_LENGTH);
|
||||
}
|
||||
}
|
||||
return Object.keys(out).length > 0 ? out : undefined;
|
||||
}
|
||||
|
||||
// Map a non-ok HTTP status to a stable error code. 400 = bad app_id/empty
|
||||
// content, 429 = rate limit (5/hour/IP). Anything else (403/404/413/5xx) is a
|
||||
// server_error so the UI never lands on an undefined state.
|
||||
export function resolveErrorCode(status: number): FeedbackErrorCode {
|
||||
if (status === 400) return 'invalid';
|
||||
if (status === 429) return 'rate_limit';
|
||||
return 'server_error';
|
||||
}
|
||||
|
||||
export async function sendFeedback(input: SendFeedbackInput): Promise<FeedbackResult> {
|
||||
const context = sanitizeContext(input.context);
|
||||
const body: Record<string, unknown> = {
|
||||
app_id: APP_ID,
|
||||
content: capContent(input.content),
|
||||
user_id: input.userId ?? null,
|
||||
};
|
||||
if (context) body.context = context;
|
||||
|
||||
try {
|
||||
const res = await fetch(`${FEEDBACK_HUB_URL}/api/feedback`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
if (res.ok) return { ok: true };
|
||||
console.warn(`[feedback] request failed with status ${res.status}`);
|
||||
return { ok: false, code: resolveErrorCode(res.status) };
|
||||
} catch (err) {
|
||||
console.warn('[feedback] network error:', err);
|
||||
return { ok: false, code: 'network_error' };
|
||||
}
|
||||
}
|
||||
149
tests/feedback.test.mjs
Normal file
149
tests/feedback.test.mjs
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
// Execution tests for the Feedback Hub client. Runs on plain node via the
|
||||
// built-in test runner + native TS type-stripping (Node >= 22.18):
|
||||
//
|
||||
// node --test tests/feedback.test.mjs
|
||||
//
|
||||
// This EXECUTES the real helpers from src/services/feedback.ts (which is kept
|
||||
// import-free so type-stripping can load it — the `@/` alias is unresolvable
|
||||
// here). Static/import-graph guards live in smoke.test.cjs.
|
||||
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {
|
||||
capContent,
|
||||
sanitizeContext,
|
||||
resolveErrorCode,
|
||||
sendFeedback,
|
||||
APP_ID,
|
||||
FEEDBACK_HUB_URL,
|
||||
MAX_CONTENT_LENGTH,
|
||||
} from '../src/services/feedback.ts';
|
||||
|
||||
test('APP_ID is the exact repo name', () => {
|
||||
assert.equal(APP_ID, 'simpl-liste');
|
||||
});
|
||||
|
||||
test('capContent trims and caps at MAX_CONTENT_LENGTH', () => {
|
||||
assert.equal(capContent(' hello '), 'hello');
|
||||
assert.equal(capContent('x'.repeat(3000)).length, MAX_CONTENT_LENGTH);
|
||||
assert.equal(capContent(''), '');
|
||||
});
|
||||
|
||||
test('sanitizeContext keeps only whitelisted string keys', () => {
|
||||
const out = sanitizeContext({
|
||||
page: '/task/1',
|
||||
locale: 'fr',
|
||||
theme: 'dark',
|
||||
secret: 'nope',
|
||||
});
|
||||
assert.deepEqual(out, { page: '/task/1', locale: 'fr', theme: 'dark' });
|
||||
});
|
||||
|
||||
test('sanitizeContext truncates values to 500 chars', () => {
|
||||
const out = sanitizeContext({ page: 'a'.repeat(900) });
|
||||
assert.equal(out.page.length, 500);
|
||||
});
|
||||
|
||||
test('sanitizeContext drops non-string values', () => {
|
||||
const out = sanitizeContext({ page: '/x', viewport: 123 });
|
||||
assert.deepEqual(out, { page: '/x' });
|
||||
});
|
||||
|
||||
test('sanitizeContext returns undefined when empty or nullish', () => {
|
||||
assert.equal(sanitizeContext(undefined), undefined);
|
||||
assert.equal(sanitizeContext(null), undefined);
|
||||
assert.equal(sanitizeContext({}), undefined);
|
||||
assert.equal(sanitizeContext({ page: '' }), undefined);
|
||||
});
|
||||
|
||||
test('resolveErrorCode maps statuses, defaulting to server_error', () => {
|
||||
assert.equal(resolveErrorCode(400), 'invalid');
|
||||
assert.equal(resolveErrorCode(429), 'rate_limit');
|
||||
assert.equal(resolveErrorCode(500), 'server_error');
|
||||
assert.equal(resolveErrorCode(413), 'server_error');
|
||||
assert.equal(resolveErrorCode(404), 'server_error');
|
||||
});
|
||||
|
||||
test('sendFeedback POSTs to the exact endpoint with the right body', async () => {
|
||||
const calls = [];
|
||||
const original = globalThis.fetch;
|
||||
globalThis.fetch = async (url, opts) => {
|
||||
calls.push({ url, opts });
|
||||
return { ok: true, status: 201 };
|
||||
};
|
||||
try {
|
||||
const result = await sendFeedback({
|
||||
content: ' Great app ',
|
||||
userId: null,
|
||||
context: { page: '/settings' },
|
||||
});
|
||||
assert.deepEqual(result, { ok: true });
|
||||
assert.equal(calls.length, 1);
|
||||
assert.equal(calls[0].url, `${FEEDBACK_HUB_URL}/api/feedback`);
|
||||
assert.equal(calls[0].opts.method, 'POST');
|
||||
assert.equal(calls[0].opts.headers['Content-Type'], 'application/json');
|
||||
const body = JSON.parse(calls[0].opts.body);
|
||||
assert.equal(body.app_id, 'simpl-liste');
|
||||
assert.equal(body.content, 'Great app'); // trimmed by capContent
|
||||
assert.equal(body.user_id, null);
|
||||
assert.deepEqual(body.context, { page: '/settings' });
|
||||
} finally {
|
||||
globalThis.fetch = original;
|
||||
}
|
||||
});
|
||||
|
||||
test('sendFeedback omits context when not opted in, and sends user_id when identified', async () => {
|
||||
const captured = [];
|
||||
const original = globalThis.fetch;
|
||||
globalThis.fetch = async (_url, opts) => {
|
||||
captured.push(JSON.parse(opts.body));
|
||||
return { ok: true, status: 201 };
|
||||
};
|
||||
try {
|
||||
await sendFeedback({ content: 'hi' });
|
||||
assert.equal('context' in captured[0], false);
|
||||
assert.equal(captured[0].user_id, null);
|
||||
|
||||
await sendFeedback({ content: 'hi', userId: 'logto-sub-123' });
|
||||
assert.equal(captured[1].user_id, 'logto-sub-123');
|
||||
} finally {
|
||||
globalThis.fetch = original;
|
||||
}
|
||||
});
|
||||
|
||||
test('sendFeedback maps error statuses to codes', async () => {
|
||||
const original = globalThis.fetch;
|
||||
const origWarn = console.warn;
|
||||
console.warn = () => {};
|
||||
try {
|
||||
for (const [status, code] of [
|
||||
[400, 'invalid'],
|
||||
[429, 'rate_limit'],
|
||||
[500, 'server_error'],
|
||||
[413, 'server_error'],
|
||||
]) {
|
||||
globalThis.fetch = async () => ({ ok: false, status });
|
||||
const result = await sendFeedback({ content: 'hi' });
|
||||
assert.deepEqual(result, { ok: false, code });
|
||||
}
|
||||
} finally {
|
||||
globalThis.fetch = original;
|
||||
console.warn = origWarn;
|
||||
}
|
||||
});
|
||||
|
||||
test('sendFeedback maps thrown fetch to network_error', async () => {
|
||||
const original = globalThis.fetch;
|
||||
const origWarn = console.warn;
|
||||
console.warn = () => {};
|
||||
globalThis.fetch = async () => {
|
||||
throw new Error('offline');
|
||||
};
|
||||
try {
|
||||
const result = await sendFeedback({ content: 'hi' });
|
||||
assert.deepEqual(result, { ok: false, code: 'network_error' });
|
||||
} finally {
|
||||
globalThis.fetch = original;
|
||||
console.warn = origWarn;
|
||||
}
|
||||
});
|
||||
|
|
@ -129,6 +129,45 @@ check('react-native-android-widget latency patch is wired up', () => {
|
|||
assert.equal(pkg.scripts.postinstall, 'patch-package', 'postinstall must run patch-package');
|
||||
});
|
||||
|
||||
// --- Feedback Hub client guards ---
|
||||
// The feedback service is load-bearing on two axes the static check protects:
|
||||
// (1) a wrong app_id/endpoint silently misroutes feedback in the shared hub,
|
||||
// and (2) it MUST stay import-free — feedback.test.mjs loads it via node:test
|
||||
// + TS type-stripping, which cannot resolve the `@/` alias or RN/Expo packages.
|
||||
// Behavioral coverage (capContent/sanitizeContext/resolveErrorCode/sendFeedback)
|
||||
// lives in tests/feedback.test.mjs; these guards lock the invariants that file
|
||||
// can't assert about the source itself.
|
||||
|
||||
const FEEDBACK_SRC = 'src/services/feedback.ts';
|
||||
|
||||
check('feedback.ts targets the exact feedback.lacompagniemaximus.com /api/feedback endpoint', () => {
|
||||
const src = fs.readFileSync(path.join(__dirname, '..', FEEDBACK_SRC), 'utf8');
|
||||
assert.ok(
|
||||
src.includes("'https://feedback.lacompagniemaximus.com'"),
|
||||
'feedback hub URL missing or changed'
|
||||
);
|
||||
assert.ok(src.includes('/api/feedback'), 'endpoint path /api/feedback missing');
|
||||
});
|
||||
|
||||
check("feedback.ts pins app_id to the exact literal 'simpl-liste'", () => {
|
||||
const src = fs.readFileSync(path.join(__dirname, '..', FEEDBACK_SRC), 'utf8');
|
||||
assert.match(src, /APP_ID\s*=\s*'simpl-liste'/, "APP_ID must be exactly 'simpl-liste'");
|
||||
});
|
||||
|
||||
check('feedback.ts caps content at 2000 chars', () => {
|
||||
const src = fs.readFileSync(path.join(__dirname, '..', FEEDBACK_SRC), 'utf8');
|
||||
assert.match(src, /MAX_CONTENT_LENGTH\s*=\s*2000/, 'content cap must be 2000');
|
||||
});
|
||||
|
||||
check('feedback.ts stays import-free (protects type-stripping test path)', () => {
|
||||
const imports = staticImportsOf(FEEDBACK_SRC);
|
||||
assert.deepEqual(
|
||||
imports,
|
||||
[],
|
||||
`feedback.ts must have zero imports, found: ${imports.join(', ')}`
|
||||
);
|
||||
});
|
||||
|
||||
if (failed === 0) {
|
||||
console.log('\nsmoke OK');
|
||||
process.exit(0);
|
||||
|
|
|
|||
Loading…
Reference in a new issue