@pi-spi/app 1.5.1 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/lib/module/l10n/app_localizations.js.map +1 -1
  2. package/lib/module/l10n/app_localizations_en.js +12 -1
  3. package/lib/module/l10n/app_localizations_en.js.map +1 -1
  4. package/lib/module/l10n/app_localizations_fr.js +12 -1
  5. package/lib/module/l10n/app_localizations_fr.js.map +1 -1
  6. package/lib/module/l10n/app_localizations_pt.js +12 -1
  7. package/lib/module/l10n/app_localizations_pt.js.map +1 -1
  8. package/lib/module/modules/alias/presentation/pages/alias_delete_btn_widget.js +15 -19
  9. package/lib/module/modules/alias/presentation/pages/alias_delete_btn_widget.js.map +1 -1
  10. package/lib/module/modules/alias/presentation/pages/alias_page_claim.js +4 -11
  11. package/lib/module/modules/alias/presentation/pages/alias_page_claim.js.map +1 -1
  12. package/lib/module/modules/compte/presentation/pages/compte_details_widget.js +6 -7
  13. package/lib/module/modules/compte/presentation/pages/compte_details_widget.js.map +1 -1
  14. package/lib/module/modules/profile/presentation/pages/compte/profile_compte_page.js +25 -11
  15. package/lib/module/modules/profile/presentation/pages/compte/profile_compte_page.js.map +1 -1
  16. package/lib/module/modules/profile/presentation/pages/parametre/profile_parametre_page.js +12 -18
  17. package/lib/module/modules/profile/presentation/pages/parametre/profile_parametre_page.js.map +1 -1
  18. package/lib/module/modules/profile/presentation/pages/parametre/profile_parametre_page_notifications.js +109 -103
  19. package/lib/module/modules/profile/presentation/pages/parametre/profile_parametre_page_notifications.js.map +1 -1
  20. package/lib/module/modules/profile/presentation/pages/parametre/profile_parametre_page_notifications_son.js +55 -72
  21. package/lib/module/modules/profile/presentation/pages/parametre/profile_parametre_page_notifications_son.js.map +1 -1
  22. package/lib/module/modules/profile/presentation/pages/parametre/profile_parametre_page_qrcode.js +31 -45
  23. package/lib/module/modules/profile/presentation/pages/parametre/profile_parametre_page_qrcode.js.map +1 -1
  24. package/lib/module/modules/profile/presentation/pages/parametre/profile_parametre_page_theme.js +103 -67
  25. package/lib/module/modules/profile/presentation/pages/parametre/profile_parametre_page_theme.js.map +1 -1
  26. package/lib/module/modules/profile/presentation/pages/profile_app_bar.js +26 -0
  27. package/lib/module/modules/profile/presentation/pages/profile_app_bar.js.map +1 -0
  28. package/lib/module/modules/profile/presentation/pages/profile_menu.js +143 -34
  29. package/lib/module/modules/profile/presentation/pages/profile_menu.js.map +1 -1
  30. package/lib/module/modules/profile/presentation/pages/profile_page.js +95 -63
  31. package/lib/module/modules/profile/presentation/pages/profile_page.js.map +1 -1
  32. package/lib/module/modules/profile/presentation/pages/profile_page_avatar.js +97 -45
  33. package/lib/module/modules/profile/presentation/pages/profile_page_avatar.js.map +1 -1
  34. package/lib/module/modules/profile/presentation/pages/profile_page_contacts.js +9 -8
  35. package/lib/module/modules/profile/presentation/pages/profile_page_contacts.js.map +1 -1
  36. package/lib/module/modules/search/presentation/pages/search_fonctionnalites.js +26 -2
  37. package/lib/module/modules/search/presentation/pages/search_fonctionnalites.js.map +1 -1
  38. package/lib/module/modules/search/presentation/pages/search_page.js +0 -1
  39. package/lib/module/modules/search/presentation/pages/search_page.js.map +1 -1
  40. package/lib/module/modules/transactions/presentation/pages/transaction_rtp/transaction_rtp_page.js +4 -11
  41. package/lib/module/modules/transactions/presentation/pages/transaction_rtp/transaction_rtp_page.js.map +1 -1
  42. package/lib/module/modules/transactions/presentation/pages/transaction_rtp/transaction_rtp_page_actions.js +13 -10
  43. package/lib/module/modules/transactions/presentation/pages/transaction_rtp/transaction_rtp_page_actions.js.map +1 -1
  44. package/lib/module/shared/utils/color.js +15 -0
  45. package/lib/module/shared/utils/color.js.map +1 -0
  46. package/lib/module/shared/widgets/error_card_widget.js +70 -0
  47. package/lib/module/shared/widgets/error_card_widget.js.map +1 -0
  48. package/lib/module/shared/widgets/selection.js +136 -0
  49. package/lib/module/shared/widgets/selection.js.map +1 -0
  50. package/lib/typescript/src/l10n/app_localizations.d.ts +22 -0
  51. package/lib/typescript/src/l10n/app_localizations.d.ts.map +1 -1
  52. package/lib/typescript/src/l10n/app_localizations_en.d.ts.map +1 -1
  53. package/lib/typescript/src/l10n/app_localizations_fr.d.ts.map +1 -1
  54. package/lib/typescript/src/l10n/app_localizations_pt.d.ts.map +1 -1
  55. package/lib/typescript/src/modules/alias/presentation/pages/alias_delete_btn_widget.d.ts.map +1 -1
  56. package/lib/typescript/src/modules/alias/presentation/pages/alias_page_claim.d.ts.map +1 -1
  57. package/lib/typescript/src/modules/compte/presentation/pages/compte_details_widget.d.ts.map +1 -1
  58. package/lib/typescript/src/modules/profile/presentation/pages/compte/profile_compte_page.d.ts.map +1 -1
  59. package/lib/typescript/src/modules/profile/presentation/pages/parametre/profile_parametre_page.d.ts.map +1 -1
  60. package/lib/typescript/src/modules/profile/presentation/pages/parametre/profile_parametre_page_notifications.d.ts.map +1 -1
  61. package/lib/typescript/src/modules/profile/presentation/pages/parametre/profile_parametre_page_notifications_son.d.ts.map +1 -1
  62. package/lib/typescript/src/modules/profile/presentation/pages/parametre/profile_parametre_page_qrcode.d.ts.map +1 -1
  63. package/lib/typescript/src/modules/profile/presentation/pages/parametre/profile_parametre_page_theme.d.ts.map +1 -1
  64. package/lib/typescript/src/modules/profile/presentation/pages/profile_app_bar.d.ts +5 -0
  65. package/lib/typescript/src/modules/profile/presentation/pages/profile_app_bar.d.ts.map +1 -0
  66. package/lib/typescript/src/modules/profile/presentation/pages/profile_menu.d.ts +31 -4
  67. package/lib/typescript/src/modules/profile/presentation/pages/profile_menu.d.ts.map +1 -1
  68. package/lib/typescript/src/modules/profile/presentation/pages/profile_page.d.ts +4 -1
  69. package/lib/typescript/src/modules/profile/presentation/pages/profile_page.d.ts.map +1 -1
  70. package/lib/typescript/src/modules/profile/presentation/pages/profile_page_avatar.d.ts +1 -1
  71. package/lib/typescript/src/modules/profile/presentation/pages/profile_page_avatar.d.ts.map +1 -1
  72. package/lib/typescript/src/modules/profile/presentation/pages/profile_page_contacts.d.ts.map +1 -1
  73. package/lib/typescript/src/modules/search/presentation/pages/search_fonctionnalites.d.ts.map +1 -1
  74. package/lib/typescript/src/modules/transactions/presentation/pages/transaction_rtp/transaction_rtp_page.d.ts.map +1 -1
  75. package/lib/typescript/src/modules/transactions/presentation/pages/transaction_rtp/transaction_rtp_page_actions.d.ts.map +1 -1
  76. package/lib/typescript/src/shared/utils/color.d.ts +7 -0
  77. package/lib/typescript/src/shared/utils/color.d.ts.map +1 -0
  78. package/lib/typescript/src/shared/widgets/error_card_widget.d.ts +11 -0
  79. package/lib/typescript/src/shared/widgets/error_card_widget.d.ts.map +1 -0
  80. package/lib/typescript/src/shared/widgets/selection.d.ts +21 -0
  81. package/lib/typescript/src/shared/widgets/selection.d.ts.map +1 -0
  82. package/package.json +1 -1
  83. package/src/l10n/app_localizations.ts +22 -0
  84. package/src/l10n/app_localizations_en.ts +11 -0
  85. package/src/l10n/app_localizations_fr.ts +11 -0
  86. package/src/l10n/app_localizations_pt.ts +11 -0
  87. package/src/modules/alias/presentation/pages/alias_delete_btn_widget.tsx +13 -12
  88. package/src/modules/alias/presentation/pages/alias_page_claim.tsx +5 -4
  89. package/src/modules/compte/presentation/pages/compte_details_widget.tsx +3 -4
  90. package/src/modules/profile/presentation/pages/compte/profile_compte_page.tsx +15 -7
  91. package/src/modules/profile/presentation/pages/parametre/profile_parametre_page.tsx +10 -12
  92. package/src/modules/profile/presentation/pages/parametre/profile_parametre_page_notifications.tsx +89 -68
  93. package/src/modules/profile/presentation/pages/parametre/profile_parametre_page_notifications_son.tsx +84 -54
  94. package/src/modules/profile/presentation/pages/parametre/profile_parametre_page_qrcode.tsx +28 -24
  95. package/src/modules/profile/presentation/pages/parametre/profile_parametre_page_theme.tsx +77 -44
  96. package/src/modules/profile/presentation/pages/profile_app_bar.tsx +21 -0
  97. package/src/modules/profile/presentation/pages/profile_menu.tsx +126 -33
  98. package/src/modules/profile/presentation/pages/profile_page.tsx +90 -66
  99. package/src/modules/profile/presentation/pages/profile_page_avatar.tsx +69 -28
  100. package/src/modules/profile/presentation/pages/profile_page_contacts.tsx +5 -4
  101. package/src/modules/search/presentation/pages/search_fonctionnalites.ts +40 -7
  102. package/src/modules/search/presentation/pages/search_page.tsx +1 -1
  103. package/src/modules/transactions/presentation/pages/transaction_rtp/transaction_rtp_page.tsx +2 -4
  104. package/src/modules/transactions/presentation/pages/transaction_rtp/transaction_rtp_page_actions.tsx +13 -6
  105. package/src/shared/utils/color.ts +14 -0
  106. package/src/shared/widgets/error_card_widget.tsx +58 -0
  107. package/src/shared/widgets/selection.tsx +98 -0
@@ -1,21 +1,22 @@
1
+ import { MaterialIcons } from '@expo/vector-icons';
1
2
  import { Image } from 'expo-image';
2
- import type { ReactNode } from 'react';
3
+ import { useState, type ReactNode } from 'react';
3
4
  import { Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
4
5
 
5
6
  import { Images } from '../../../../../core/assets';
6
7
  import { useBloc, useBlocBuilder } from '../../../../../core/bloc_react';
7
- import { AppRouter, useAppRouter } from '../../../../../core/router';
8
8
  import { Themer, Themes, useTheme, type ThemeData } from '../../../../../core/theme';
9
9
  import type { AppLocalizations } from '../../../../../l10n/app_localizations';
10
10
  import { useAppLocalizations } from '../../../../../l10n/localizations';
11
11
  import { Card } from '../../../../../shared/material/card';
12
- import { AppBar, Scaffold } from '../../../../../shared/material/scaffold';
12
+ import { Scaffold } from '../../../../../shared/material/scaffold';
13
13
  import { MyPageContainer } from '../../../../../shared/widgets/my_page_container';
14
14
  import { ConfigBloc } from '../../../../config/adapters/ui/bloc/config_bloc';
15
15
  import { ConfigChangeEvent } from '../../../../config/adapters/ui/bloc/config_event';
16
16
  import { ConfigLoadedState } from '../../../../config/adapters/ui/bloc/config_state';
17
17
  import { ConfigKey } from '../../../../config/domain/models/config_keys';
18
- import { CheckedDot } from './settings_widgets';
18
+ import { ProfileAppBar } from '../profile_app_bar';
19
+ import { ProfileSectionTitre } from '../profile_menu';
19
20
 
20
21
  function themeIcon(theme: Themes): number {
21
22
  switch (theme) {
@@ -49,8 +50,6 @@ function themeTitle(traductions: AppLocalizations, theme: Themes): string {
49
50
  /** Choix du thème de l'application (route `profileParametreTheme`). */
50
51
  export function ProfileParametrePageTheme() {
51
52
  const traductions = useAppLocalizations();
52
- const theme = useTheme();
53
- const router = useAppRouter();
54
53
  const configBloc = useBloc(ConfigBloc);
55
54
  const state = useBlocBuilder(
56
55
  configBloc,
@@ -58,34 +57,43 @@ export function ProfileParametrePageTheme() {
58
57
  );
59
58
  const currentTheme = state.configParams.params[ConfigKey.preferedTheme];
60
59
 
60
+ const [largeurGrille, setLargeurGrille] = useState(0);
61
+ // Trois aperçus par ligne
62
+ // Arrondi vers le bas : sinon la troisième colonne peut passer à la ligne
63
+ const largeur = Math.floor((largeurGrille - 2 * ESPACE) / 3);
64
+
61
65
  return (
62
- <Scaffold appBar={<AppBar onBack={() => AppRouter.pop(router)} />}>
66
+ <Scaffold appBar={<ProfileAppBar titre={traductions.appSettingPageMenuThemePageTitle} />}>
63
67
  <MyPageContainer>
64
- <ScrollView>
65
- <Text style={theme.textTheme.titleSmall}>{traductions.appSettingPageMenuThemePageTitle}</Text>
66
- <View style={styles.gap20} />
67
- <Text style={theme.textTheme.headlineMedium}>{traductions.appSettingPageMenuThemeTitle}</Text>
68
- <View style={styles.gap10} />
69
- <Card>
70
- <View style={styles.wrap}>
71
- {/* Thème personnalisé de l'hôte, en tête */}
72
- {Themer.customTheme != null && (
73
- <ThemeOption
74
- title={traductions.appSettingPageMenuThemeCustom}
75
- selected={currentTheme === Themer.customThemeCode}
76
- onSelect={() => configBloc.add(new ConfigChangeEvent(ConfigKey.preferedTheme, Themer.customThemeCode))}
77
- preview={<CustomThemePreview theme={Themer.customTheme} />}
78
- />
68
+ <ScrollView contentContainerStyle={styles.contenu}>
69
+ {/* Thème */}
70
+ <ProfileSectionTitre titre={traductions.appSettingPageMenuThemeTitle} />
71
+ <Card style={styles.card}>
72
+ <View style={styles.grille} onLayout={(e) => setLargeurGrille(e.nativeEvent.layout.width - 2 * 14)}>
73
+ {largeurGrille > 0 && (
74
+ <>
75
+ {/* Thème personnalisé de l'hôte, en tête */}
76
+ {Themer.customTheme != null && (
77
+ <ThemeOption
78
+ largeur={largeur}
79
+ title={traductions.appSettingPageMenuThemeCustom}
80
+ selected={currentTheme === Themer.customThemeCode}
81
+ onSelect={() => configBloc.add(new ConfigChangeEvent(ConfigKey.preferedTheme, Themer.customThemeCode))}
82
+ preview={<CustomThemePreview theme={Themer.customTheme} />}
83
+ />
84
+ )}
85
+ {Themes.list().map((item) => (
86
+ <ThemeOption
87
+ key={item}
88
+ largeur={largeur}
89
+ title={themeTitle(traductions, item)}
90
+ selected={currentTheme === item}
91
+ onSelect={() => configBloc.add(new ConfigChangeEvent(ConfigKey.preferedTheme, item))}
92
+ preview={<Image source={themeIcon(item)} style={styles.flex} contentFit="cover" />}
93
+ />
94
+ ))}
95
+ </>
79
96
  )}
80
- {Themes.list().map((item) => (
81
- <ThemeOption
82
- key={item}
83
- title={themeTitle(traductions, item)}
84
- selected={currentTheme === item}
85
- onSelect={() => configBloc.add(new ConfigChangeEvent(ConfigKey.preferedTheme, item))}
86
- preview={<Image source={themeIcon(item)} style={styles.flex} contentFit="cover" />}
87
- />
88
- ))}
89
97
  </View>
90
98
  </Card>
91
99
  </ScrollView>
@@ -94,13 +102,15 @@ export function ProfileParametrePageTheme() {
94
102
  );
95
103
  }
96
104
 
97
- /** Une option de thème : aperçu, nom et pastille de sélection */
105
+ /** Une option de thème : aperçu, entouré et coché quand il est choisi, puis son nom */
98
106
  function ThemeOption({
107
+ largeur,
99
108
  title,
100
109
  selected,
101
110
  onSelect,
102
111
  preview,
103
112
  }: {
113
+ largeur: number;
104
114
  title: string;
105
115
  selected: boolean;
106
116
  onSelect: () => void;
@@ -108,13 +118,32 @@ function ThemeOption({
108
118
  }) {
109
119
  const theme = useTheme();
110
120
  return (
111
- <Pressable accessibilityRole="button" disabled={selected} onPress={onSelect} style={styles.item}>
112
- <View style={[styles.preview, { backgroundColor: theme.colorScheme.surface }]}>{preview}</View>
113
- <View style={styles.gap5} />
114
- <Text style={theme.textTheme.displayMedium}>{title}</Text>
115
- <View style={styles.gap5} />
116
- <CheckedDot value={selected} />
117
- <View style={styles.gap20} />
121
+ <Pressable
122
+ accessibilityRole="button"
123
+ accessibilityState={{ selected }}
124
+ disabled={selected}
125
+ onPress={onSelect}
126
+ style={[styles.item, { width: largeur }]}
127
+ >
128
+ {/* Aperçu, entouré quand il est choisi */}
129
+ <View style={[styles.cadre, { borderColor: selected ? theme.colorScheme.primary : 'transparent' }]}>
130
+ <View style={[styles.apercu, { backgroundColor: theme.colorScheme.surface }]}>{preview}</View>
131
+ </View>
132
+ {selected && (
133
+ <View style={[styles.coche, { backgroundColor: theme.colorScheme.primary, borderColor: theme.colorScheme.surface }]}>
134
+ <MaterialIcons name="check" size={14} color={theme.colorScheme.onPrimary} />
135
+ </View>
136
+ )}
137
+ <Text
138
+ style={[
139
+ theme.textTheme.displayMedium,
140
+ styles.libelle,
141
+ { fontWeight: selected ? '700' : '500' },
142
+ selected ? null : { color: Themer.neural03Color },
143
+ ]}
144
+ >
145
+ {title}
146
+ </Text>
118
147
  </Pressable>
119
148
  );
120
149
  }
@@ -159,6 +188,8 @@ function CustomThemePreview({ theme }: { theme: ThemeData }) {
159
188
  );
160
189
  }
161
190
 
191
+ const ESPACE = 12;
192
+
162
193
  const styles = StyleSheet.create({
163
194
  flex: { flex: 1 },
164
195
  mock: { padding: 10 },
@@ -172,10 +203,12 @@ const styles = StyleSheet.create({
172
203
  dot14: { width: 14, height: 14, borderRadius: 7 },
173
204
  gap12: { height: 12 },
174
205
  gap14: { height: 14 },
175
- wrap: { padding: 20, flexDirection: 'row', flexWrap: 'wrap', justifyContent: 'space-between', columnGap: 10, rowGap: 10 },
206
+ contenu: { paddingTop: 8, paddingBottom: 16 },
207
+ card: { margin: 0 },
208
+ grille: { padding: 14, flexDirection: 'row', flexWrap: 'wrap', columnGap: ESPACE, rowGap: 16 },
176
209
  item: { alignItems: 'center' },
177
- preview: { width: 120, height: 200, borderRadius: 8, overflow: 'hidden' },
178
- gap5: { height: 5 },
179
- gap10: { height: 10 },
180
- gap20: { height: 20 },
210
+ cadre: { alignSelf: 'stretch', padding: 3, borderWidth: 2, borderRadius: 14 },
211
+ apercu: { width: '100%', aspectRatio: 0.6, borderRadius: 10, overflow: 'hidden' },
212
+ coche: { position: 'absolute', top: 8, right: 8, borderRadius: 12, borderWidth: 2, padding: 1 },
213
+ libelle: { marginTop: 8, textAlign: 'center' },
181
214
  });
@@ -0,0 +1,21 @@
1
+ import { Text } from 'react-native';
2
+
3
+ import { AppRouter, useAppRouter } from '../../../../core/router';
4
+ import { useTheme } from '../../../../core/theme';
5
+ import { AppBar } from '../../../../shared/material/scaffold';
6
+
7
+ /** Barre des écrans du profil : bouton retour et [titre] de la page. */
8
+ export function ProfileAppBar({ titre }: { titre: string }) {
9
+ const theme = useTheme();
10
+ const router = useAppRouter();
11
+ return (
12
+ <AppBar
13
+ onBack={() => AppRouter.pop(router)}
14
+ title={
15
+ <Text numberOfLines={1} style={[theme.textTheme.headlineMedium, { color: theme.appBarTheme.foregroundColor }]}>
16
+ {titre}
17
+ </Text>
18
+ }
19
+ />
20
+ );
21
+ }
@@ -1,11 +1,13 @@
1
+ import { MaterialIcons } from '@expo/vector-icons';
1
2
  import { Image } from 'expo-image';
2
- import type { ReactNode } from 'react';
3
- import { Pressable, StyleSheet, Switch, Text, View } from 'react-native';
3
+ import { Fragment, isValidElement, type ComponentProps, type ReactElement, type ReactNode } from 'react';
4
+ import { Platform, Pressable, StyleSheet, Switch, Text, View } from 'react-native';
4
5
 
5
6
  import { AppRouter, useAppRouter } from '../../../../core/router';
6
- import { useTheme } from '../../../../core/theme';
7
+ import { Themer, useTheme } from '../../../../core/theme';
7
8
  import { Card } from '../../../../shared/material/card';
8
9
  import { showModalBottomSheet } from '../../../../shared/material/overlays';
10
+ import { withAlpha } from '../../../../shared/utils/color';
9
11
 
10
12
  /** Une entrée du menu du profil : une route, une action, ou une feuille à ouvrir. */
11
13
  export class ProfileItem {
@@ -27,45 +29,136 @@ export class ProfileItem {
27
29
  }
28
30
  }
29
31
 
30
- /** Menu du profil : une carte avec une ligne (icône, titre, sous-titre) par entrée. */
31
- export function ProfilePageMenu({ items }: { items: ProfileItem[] }) {
32
+ /**
33
+ * Groupe d'entrées du profil dans une carte : [titre] de section facultatif
34
+ * au-dessus, puis une ligne par élément, séparées par un trait. Les [items]
35
+ * sont des `ProfileItem` ou des composants déjà construits (ex. bouton de
36
+ * modification du code PIN, qui utilise `ProfileMenuTile`).
37
+ */
38
+ export function ProfilePageMenu({ items, titre }: { items: (ProfileItem | ReactElement)[]; titre?: string }) {
32
39
  const theme = useTheme();
33
40
  const router = useAppRouter();
34
- return (
35
- <Card>
36
- {items.map((item, i) => (
37
- <Pressable
38
- key={i}
39
- accessibilityRole="button"
41
+ const lignes = items
42
+ .map((item) => {
43
+ if (!(item instanceof ProfileItem)) return isValidElement(item) ? item : null;
44
+ const aSwitch = item.action != null && item.value != null;
45
+ return (
46
+ <ProfileMenuTile
47
+ image={item.icon}
48
+ titre={item.title}
49
+ sousTitre={item.subtitle}
40
50
  onPress={() => {
41
51
  if (typeof item.page === 'string') void AppRouter.push(router, item.page);
42
52
  else if (typeof item.page === 'function') item.page();
43
53
  // Ouvrir une feuille
44
- else if (item.sheet != null) void showModalBottomSheet(router, { themed: true, builder: item.sheet });
54
+ else if (item.sheet != null) void showModalBottomSheet(router, { builder: item.sheet });
45
55
  }}
46
- style={({ pressed }) => [styles.tile, pressed ? { backgroundColor: theme.colorScheme.secondary } : null]}
47
- >
48
- <Image source={item.icon} style={styles.icon} tintColor={theme.colorScheme.onSurface} />
49
- <View style={styles.texts}>
50
- <Text style={theme.textTheme.bodyLarge}>{item.title}</Text>
51
- {item.subtitle != null && <Text style={theme.textTheme.displaySmall}>{item.subtitle}</Text>}
52
- </View>
53
- {item.action != null && item.value != null && (
54
- <Switch
55
- value={item.value}
56
- onValueChange={item.action}
57
- trackColor={{ false: theme.switchTheme.inactiveTrackColor, true: theme.switchTheme.activeTrackColor }}
58
- thumbColor={theme.switchTheme.thumbColor}
59
- />
60
- )}
61
- </Pressable>
62
- ))}
63
- </Card>
56
+ trailing={
57
+ aSwitch ? (
58
+ <Switch
59
+ value={item.value}
60
+ onValueChange={item.action}
61
+ trackColor={{ false: theme.switchTheme.inactiveTrackColor, true: theme.switchTheme.activeTrackColor }}
62
+ thumbColor={theme.switchTheme.thumbColor}
63
+ />
64
+ ) : undefined
65
+ }
66
+ />
67
+ );
68
+ })
69
+ .filter((ligne): ligne is ReactElement => ligne != null);
70
+
71
+ return (
72
+ <View>
73
+ {titre != null && <ProfileSectionTitre titre={titre} />}
74
+ <Card style={styles.card}>
75
+ {lignes.map((ligne, i) => (
76
+ <Fragment key={i}>
77
+ {i > 0 && <View style={styles.separateur} />}
78
+ {ligne}
79
+ </Fragment>
80
+ ))}
81
+ </Card>
82
+ </View>
83
+ );
84
+ }
85
+
86
+ /**
87
+ * Ligne d'un menu du profil : pastille avec l'icône, [titre], [sousTitre]
88
+ * facultatif et, à droite, [trailing] (ex. Switch) ou une flèche.
89
+ * [couleur] remplace la couleur principale (ex. rouge pour une action
90
+ * destructive).
91
+ */
92
+ export function ProfileMenuTile({
93
+ image,
94
+ icone,
95
+ titre,
96
+ sousTitre,
97
+ onPress,
98
+ trailing,
99
+ couleur,
100
+ disabled,
101
+ }: {
102
+ image?: number;
103
+ icone?: ComponentProps<typeof MaterialIcons>['name'];
104
+ titre: string;
105
+ sousTitre?: string;
106
+ onPress?: () => void;
107
+ trailing?: ReactNode;
108
+ couleur?: string;
109
+ disabled?: boolean;
110
+ }) {
111
+ const theme = useTheme();
112
+ const principale = couleur ?? theme.colorScheme.primary;
113
+ return (
114
+ <Pressable
115
+ accessibilityRole="button"
116
+ disabled={disabled}
117
+ onPress={onPress}
118
+ style={({ pressed }) => [styles.tile, pressed ? { backgroundColor: theme.colorScheme.secondary } : null]}
119
+ >
120
+ {/* Pastille */}
121
+ <View style={[styles.pastille, { backgroundColor: withAlpha(principale, 0.14) }]}>
122
+ {image != null ? (
123
+ <Image source={image} style={styles.icon} tintColor={couleur ?? theme.colorScheme.onSurface} />
124
+ ) : (
125
+ <MaterialIcons name={icone ?? 'circle'} size={20} color={principale} />
126
+ )}
127
+ </View>
128
+ <View style={styles.texts}>
129
+ <Text style={[theme.textTheme.displayLarge, styles.titre, couleur != null ? { color: couleur } : null]}>{titre}</Text>
130
+ {sousTitre != null && sousTitre.length > 0 && (
131
+ <Text style={[theme.textTheme.displaySmall, styles.sousTitre, { color: Themer.neural03Color }]}>{sousTitre}</Text>
132
+ )}
133
+ </View>
134
+ {trailing ?? (
135
+ <MaterialIcons
136
+ name={Platform.OS === 'ios' ? 'arrow-forward-ios' : 'arrow-forward'}
137
+ size={18}
138
+ color={couleur ?? Themer.neural03Color}
139
+ />
140
+ )}
141
+ </Pressable>
142
+ );
143
+ }
144
+
145
+ /** Titre d'une section du profil, en petites capitales. */
146
+ export function ProfileSectionTitre({ titre }: { titre: string }) {
147
+ const theme = useTheme();
148
+ return (
149
+ <Text style={[theme.textTheme.displaySmall, styles.section, { color: Themer.neural03Color }]}>{titre.toUpperCase()}</Text>
64
150
  );
65
151
  }
66
152
 
67
153
  const styles = StyleSheet.create({
68
- tile: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, minHeight: 56, paddingVertical: 8 },
69
- icon: { width: 24, height: 24 },
70
- texts: { flex: 1, marginLeft: 16 },
154
+ card: { margin: 0 },
155
+ tile: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 14, paddingVertical: 12 },
156
+ pastille: { width: 40, height: 40, borderRadius: 12, alignItems: 'center', justifyContent: 'center' },
157
+ icon: { width: 20, height: 20 },
158
+ texts: { flex: 1, marginLeft: 14, marginRight: 8 },
159
+ titre: { fontWeight: '500' },
160
+ sousTitre: { marginTop: 2 },
161
+ section: { fontSize: 12, letterSpacing: 0.8, fontWeight: '600', marginLeft: 4, marginBottom: 8 },
162
+ // Trait aligné sur le texte (après la pastille)
163
+ separateur: { height: StyleSheet.hairlineWidth, marginLeft: 68, backgroundColor: withAlpha(Themer.neural03Color, 0.25) },
71
164
  });
@@ -1,4 +1,4 @@
1
- import { Linking, Platform, StyleSheet, Text, View } from 'react-native';
1
+ import { Linking, Platform, Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
2
2
 
3
3
  import { Images } from '../../../../core/assets';
4
4
  import { AppEnv } from '../../../../core/env';
@@ -7,9 +7,10 @@ import { AppRouter, useAppRouter } from '../../../../core/router';
7
7
  import { Themer, useTheme } from '../../../../core/theme';
8
8
  import type { AppLocalizations } from '../../../../l10n/app_localizations';
9
9
  import { useAppLocalizations } from '../../../../l10n/localizations';
10
- import { TextButton } from '../../../../shared/material/buttons';
11
- import { AppBar, Scaffold } from '../../../../shared/material/scaffold';
10
+ import { Scaffold } from '../../../../shared/material/scaffold';
11
+ import { withAlpha } from '../../../../shared/utils/color';
12
12
  import { MyPageContainer } from '../../../../shared/widgets/my_page_container';
13
+ import { ProfileAppBar } from './profile_app_bar';
13
14
  import { ProfilePageMenu, ProfileItem } from './profile_menu';
14
15
  import { ProfilePageAvatar } from './profile_page_avatar';
15
16
 
@@ -23,7 +24,7 @@ async function launchUrl(url: string): Promise<void> {
23
24
  }
24
25
  }
25
26
 
26
- function menuItems(traductions: AppLocalizations): ProfileItem[] {
27
+ function menuCompteItems(traductions: AppLocalizations): ProfileItem[] {
27
28
  const items = [
28
29
  // Compte
29
30
  new ProfileItem(Images.iconsCircleUser, traductions.profilePageMenuCompteTitle, AppRouter.profileCompte),
@@ -34,84 +35,107 @@ function menuItems(traductions: AppLocalizations): ProfileItem[] {
34
35
  if (Platform.OS !== 'web') {
35
36
  items.push(new ProfileItem(Images.iconTransactionSendByAlias, traductions.profilePageMenuAliasContactTitle, AppRouter.profileContacts));
36
37
  }
37
- // Centre d'aide
38
- if (AppEnv.aideLink != null && AppEnv.aideLink.length > 0) {
39
- const aideLink = AppEnv.aideLink;
40
- items.push(
41
- new ProfileItem(
42
- Images.iconsHelp,
43
- traductions.profilePageMenuHelpTitle,
44
- Platform.OS !== 'web' ? AppRouter.profileHelpCenter : () => void launchUrl(aideLink),
45
- ),
46
- );
47
- }
48
38
  return items;
49
39
  }
50
40
 
51
- /** Profil du client (route `profile`). */
41
+ function menuAssistanceItems(traductions: AppLocalizations): ProfileItem[] {
42
+ // Centre d'aide (seulement si l'hôte fournit un lien)
43
+ const aideLink = AppEnv.aideLink;
44
+ if (aideLink == null || aideLink.length === 0) return [];
45
+ return [
46
+ new ProfileItem(
47
+ Images.iconsHelp,
48
+ traductions.profilePageMenuHelpTitle,
49
+ Platform.OS !== 'web' ? AppRouter.profileHelpCenter : () => void launchUrl(aideLink),
50
+ ),
51
+ ];
52
+ }
53
+
54
+ /**
55
+ * Profil du client (route `profile`) : en-tête de l'utilisateur, menus
56
+ * « Mon compte » et « Assistance », puis liens légaux et version de l'app.
57
+ */
52
58
  export function ProfilePage() {
53
59
  const traductions = useAppLocalizations();
54
60
  const theme = useTheme();
55
61
  const router = useAppRouter();
56
- const hasCgu = AppEnv.cguLink != null && AppEnv.cguLink.length > 0;
57
- const hasPoc = AppEnv.pocLink != null && AppEnv.pocLink.length > 0;
58
- const linkStyle = { fontSize: 12, fontWeight: '400' as const };
62
+ const assistance = menuAssistanceItems(traductions);
63
+ // CGU et politique de confidentialité optionnelles : l'hôte peut ne pas en fournir
64
+ const cguLink = AppEnv.cguLink;
65
+ const pocLink = AppEnv.pocLink;
66
+ const hasCgu = cguLink != null && cguLink.length > 0;
67
+ const hasPoc = pocLink != null && pocLink.length > 0;
68
+ const lienStyle = [
69
+ theme.textTheme.displaySmall,
70
+ { color: Themer.neural03Color, textDecorationLine: 'underline' as const, textAlign: 'center' as const },
71
+ ];
72
+
73
+ const lien = (texte: string, onPress: () => void) => (
74
+ <Pressable accessibilityRole="link" onPress={onPress} style={styles.lien}>
75
+ <Text style={lienStyle}>{texte}</Text>
76
+ </Pressable>
77
+ );
59
78
 
60
79
  return (
61
- <Scaffold appBar={<AppBar onBack={() => AppRouter.pop(router)} />}>
80
+ <Scaffold appBar={<ProfileAppBar titre={traductions.profilePageTitle} />}>
62
81
  <MyPageContainer>
63
- {/* Client connecté */}
64
- <ProfilePageAvatar />
65
- <View style={styles.gap20} />
66
- {/* Menu */}
67
- <ProfilePageMenu items={menuItems(traductions)} />
68
- <View style={styles.flex} />
69
- {/* CGU et politique de confidentialité (optionnels) */}
70
- {(hasCgu || hasPoc) && (
71
- <View style={styles.links}>
72
- {hasCgu && (
73
- <View style={styles.flex}>
74
- <TextButton
75
- textStyle={linkStyle}
76
- onPress={() => (Platform.OS !== 'web' ? void AppRouter.push(router, AppRouter.loginCgu) : void launchUrl(AppEnv.cguLink!))}
77
- >
78
- {traductions.profilePageCGU}
79
- </TextButton>
80
- </View>
81
- )}
82
- {hasCgu && hasPoc && (
83
- <Text style={[theme.textTheme.displayLarge, styles.et, { color: Themer.neural03Color }]}>{traductions.coordinationEt}</Text>
84
- )}
85
- {hasPoc && (
86
- <View style={styles.flex}>
87
- <TextButton
88
- textStyle={linkStyle}
89
- onPress={() => (Platform.OS !== 'web' ? void AppRouter.push(router, AppRouter.loginPoc) : void launchUrl(AppEnv.pocLink!))}
90
- >
91
- {traductions.profilePagePC}
92
- </TextButton>
82
+ <ScrollView contentContainerStyle={styles.contenu} showsVerticalScrollIndicator={false}>
83
+ {/* Utilisateur connecté */}
84
+ <ProfilePageAvatar />
85
+ <View style={styles.gap28} />
86
+
87
+ {/* Compte, paramètres, contacts */}
88
+ <ProfilePageMenu titre={traductions.profilePageSectionCompte} items={menuCompteItems(traductions)} />
89
+ <View style={styles.gap24} />
90
+
91
+ {/* Aide (seulement si l'hôte fournit un lien) */}
92
+ {assistance.length > 0 && (
93
+ <>
94
+ <ProfilePageMenu titre={traductions.profilePageSectionAssistance} items={assistance} />
95
+ <View style={styles.gap24} />
96
+ </>
97
+ )}
98
+
99
+ {/* CGU et politique de confidentialité, côte à côte ou l'un sous l'autre */}
100
+ {(hasCgu || hasPoc) && (
101
+ <>
102
+ <View style={styles.liens}>
103
+ {hasCgu &&
104
+ lien(traductions.profilePageCGU, () =>
105
+ Platform.OS !== 'web' ? void AppRouter.push(router, AppRouter.loginCgu) : void launchUrl(cguLink),
106
+ )}
107
+ {hasPoc &&
108
+ lien(traductions.profilePagePC, () =>
109
+ Platform.OS !== 'web' ? void AppRouter.push(router, AppRouter.loginPoc) : void launchUrl(pocLink),
110
+ )}
93
111
  </View>
94
- )}
95
- </View>
96
- )}
97
- {/* Version (lien vers le store) */}
98
- <TextButton
99
- textStyle={[theme.textTheme.displaySmall, styles.version]}
100
- onPress={() => void launchUrl(Platform.OS === 'ios' ? AppEnv.appstoreLink : AppEnv.playstoreLink)}
101
- >
102
- {`${traductions.profilePageAppVersion} ${AppEnv.version}`}
103
- </TextButton>
104
- <View style={styles.gap10} />
112
+ <View style={styles.gap10} />
113
+ </>
114
+ )}
115
+
116
+ {/* Version : ouvre la fiche de l'app sur le store */}
117
+ <Pressable
118
+ accessibilityRole="link"
119
+ disabled={Platform.OS === 'web'}
120
+ onPress={() => void launchUrl(Platform.OS === 'ios' ? AppEnv.appstoreLink : AppEnv.playstoreLink)}
121
+ style={[styles.version, { backgroundColor: withAlpha(Themer.neural03Color, 0.12) }]}
122
+ >
123
+ <Text style={[theme.textTheme.labelLarge, { color: Themer.neural03Color }]}>
124
+ {`${traductions.profilePageAppVersion} ${AppEnv.version}`}
125
+ </Text>
126
+ </Pressable>
127
+ </ScrollView>
105
128
  </MyPageContainer>
106
129
  </Scaffold>
107
130
  );
108
131
  }
109
132
 
110
133
  const styles = StyleSheet.create({
111
- flex: { flex: 1 },
112
- links: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 20, paddingVertical: 10 },
113
- et: { paddingHorizontal: 5 },
114
- version: { color: '#2196F3', textDecorationLine: 'underline' },
134
+ contenu: { paddingTop: 4, paddingBottom: 16 },
135
+ liens: { flexDirection: 'row', flexWrap: 'wrap', justifyContent: 'center', alignItems: 'center', columnGap: 12 },
136
+ lien: { paddingHorizontal: 4, paddingVertical: 6, borderRadius: 6 },
137
+ version: { alignSelf: 'center', paddingHorizontal: 12, paddingVertical: 6, borderRadius: 20 },
115
138
  gap10: { height: 10 },
116
- gap20: { height: 20 },
139
+ gap24: { height: 24 },
140
+ gap28: { height: 28 },
117
141
  });