@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,6 +1,7 @@
1
1
  import { MaterialIcons } from '@expo/vector-icons';
2
- import { useState } from 'react';
2
+ import { useId, useState } from 'react';
3
3
  import { Pressable, StyleSheet, Text, View } from 'react-native';
4
+ import Svg, { Defs, LinearGradient, Rect, Stop } from 'react-native-svg';
4
5
 
5
6
  import { useBloc } from '../../../../core/bloc_react';
6
7
  import { Di } from '../../../../core/di';
@@ -9,16 +10,18 @@ import { Themer, useTheme } from '../../../../core/theme';
9
10
  import { useValueListenable } from '../../../../core/value_notifier';
10
11
  import { useAppLocalizations } from '../../../../l10n/localizations';
11
12
  import { showModalBottomSheet } from '../../../../shared/material/overlays';
12
- import { AliasWidget } from '../../../../shared/widgets/alias_widget';
13
+ import { AliasPI } from '../../../../shared/models/alias_pi';
14
+ import { withAlpha } from '../../../../shared/utils/color';
13
15
  import { AvatarWidget } from '../../../../shared/widgets/avatar_widget';
16
+ import { ClipboardWidget } from '../../../../shared/widgets/clipboard_widget';
14
17
  import { ImageSelectWidget } from '../../../../shared/widgets/image_select_widget';
15
18
  import { AliasBloc } from '../../../alias/presentation/bloc/alias_bloc';
16
19
  import { AliasExistState } from '../../../alias/presentation/bloc/alias_state';
17
20
  import { ProfileAvatar } from '../../domain/profile_avatar';
18
21
 
19
- const AVATAR_SIZE = 64;
22
+ const AVATAR_SIZE = 68;
20
23
 
21
- /** En-tête du profil : nom, alias, et photo (modifiable). */
24
+ /** En-tête du profil : photo (modifiable), nom et alias. */
22
25
  export function ProfilePageAvatar() {
23
26
  const traductions = useAppLocalizations();
24
27
  const theme = useTheme();
@@ -27,6 +30,7 @@ export function ProfilePageAvatar() {
27
30
  const [aliasCle] = useState(() => (aliasBloc.state instanceof AliasExistState ? aliasBloc.state.alias.getCle() : ''));
28
31
  const user = Di.config.user;
29
32
  const avatar = useValueListenable(ProfileAvatar.value);
33
+ const gradientId = `entete${useId().replace(/[^a-zA-Z0-9]/g, '')}`;
30
34
 
31
35
  const showImagePicker = () =>
32
36
  void showModalBottomSheet(router, {
@@ -63,49 +67,86 @@ export function ProfilePageAvatar() {
63
67
  ),
64
68
  });
65
69
 
70
+ const primaire = theme.colorScheme.primary;
71
+
66
72
  return (
67
- <View style={styles.row}>
68
- {/* Nom et alias */}
69
- <View style={styles.flex}>
70
- <Text numberOfLines={1} style={theme.textTheme.titleSmall}>
71
- {user.nomComplet()}
72
- </Text>
73
- <View style={styles.gap8} />
74
- <AliasWidget alias={aliasCle} name={user.nomComplet()} canCopy textAlign="left" />
75
- </View>
76
- <View style={styles.gap8w} />
73
+ <View style={styles.entete}>
74
+ {/* Dégradé doux de la couleur du thème */}
75
+ <Svg style={StyleSheet.absoluteFill} preserveAspectRatio="none">
76
+ <Defs>
77
+ <LinearGradient id={gradientId} x1="0" y1="0" x2="1" y2="1">
78
+ <Stop offset="0" stopColor={primaire} stopOpacity={0.22} />
79
+ <Stop offset="1" stopColor={primaire} stopOpacity={0.06} />
80
+ </LinearGradient>
81
+ </Defs>
82
+ <Rect width="100%" height="100%" fill={`url(#${gradientId})`} />
83
+ </Svg>
84
+
77
85
  {/* Avatar */}
78
86
  <Pressable accessibilityRole="imagebutton" onPress={showImagePicker}>
79
- <View style={styles.shadow}>
80
- <AvatarWidget userInitiales={user.initiales()} photo={avatar} size={AVATAR_SIZE} rounded />
87
+ <View style={[styles.avatar, { backgroundColor: theme.colorScheme.surface }]}>
88
+ <AvatarWidget userInitiales={user.initiales()} photo={avatar} size={AVATAR_SIZE - 6} rounded />
81
89
  </View>
82
90
  {/* Badge caméra */}
83
- <View style={[styles.camera, { backgroundColor: theme.colorScheme.primary, borderColor: theme.scaffoldBackgroundColor }]}>
84
- <MaterialIcons name="photo-camera" size={14} color="white" />
91
+ <View style={[styles.camera, { backgroundColor: primaire, borderColor: theme.colorScheme.surface }]}>
92
+ <MaterialIcons name="photo-camera" size={15} color={theme.colorScheme.onPrimary} />
85
93
  </View>
86
94
  </Pressable>
95
+
96
+ {/* Nom */}
97
+ <Text numberOfLines={2} style={[theme.textTheme.headlineMedium, styles.nom]}>
98
+ {user.nomComplet()}
99
+ </Text>
100
+
101
+ {/* Alias de l'utilisateur, copié d'un appui */}
102
+ <Pressable
103
+ accessibilityRole="button"
104
+ onPress={() => void ClipboardWidget.setData(aliasCle)}
105
+ style={[styles.alias, { backgroundColor: withAlpha(theme.colorScheme.surface, 0.7) }]}
106
+ >
107
+ <MaterialIcons name="badge" size={15} color={primaire} />
108
+ <Text numberOfLines={1} style={[theme.textTheme.displayMedium, styles.aliasTexte]}>
109
+ {AliasPI.formatPhoneNumber(aliasCle)}
110
+ </Text>
111
+ <MaterialIcons name="content-copy" size={14} color={primaire} />
112
+ </Pressable>
87
113
  </View>
88
114
  );
89
115
  }
90
116
 
91
117
  const styles = StyleSheet.create({
92
- row: { flexDirection: 'row', alignItems: 'center' },
93
- flex: { flex: 1 },
94
- gap8: { height: 8 },
95
- gap8w: { width: 8 },
96
- shadow: { borderRadius: AVATAR_SIZE / 2, boxShadow: '0 3px 8px rgba(0,0,0,0.15)' },
118
+ entete: {
119
+ alignItems: 'center',
120
+ paddingTop: 24,
121
+ paddingBottom: 20,
122
+ paddingHorizontal: 20,
123
+ borderRadius: 24,
124
+ overflow: 'hidden',
125
+ },
126
+ avatar: { padding: 3, borderRadius: AVATAR_SIZE / 2, boxShadow: '0 3px 12px rgba(0,0,0,0.12)' },
97
127
  camera: {
98
128
  position: 'absolute',
99
- right: -4,
100
- bottom: -4,
101
- width: 28,
102
- height: 28,
103
- borderRadius: 14,
129
+ right: 0,
130
+ bottom: 0,
131
+ width: 30,
132
+ height: 30,
133
+ borderRadius: 15,
104
134
  borderWidth: 2,
105
135
  alignItems: 'center',
106
136
  justifyContent: 'center',
107
137
  boxShadow: '0 2px 4px rgba(0,0,0,0.2)',
108
138
  },
139
+ nom: { marginTop: 14, textAlign: 'center' },
140
+ alias: {
141
+ flexDirection: 'row',
142
+ alignItems: 'center',
143
+ marginTop: 12,
144
+ paddingHorizontal: 12,
145
+ paddingVertical: 5,
146
+ borderRadius: 20,
147
+ maxWidth: '100%',
148
+ },
149
+ aliasTexte: { flexShrink: 1, marginHorizontal: 6 },
109
150
  deleteTile: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, minHeight: 56 },
110
151
  deleteText: { flex: 1, marginHorizontal: 16 },
111
152
  });
@@ -10,13 +10,14 @@ import { Themer, useTheme } from '../../../../core/theme';
10
10
  import { useAppLocalizations } from '../../../../l10n/localizations';
11
11
  import { Card } from '../../../../shared/material/card';
12
12
  import { showModalBottomSheet } from '../../../../shared/material/overlays';
13
- import { AppBar, Scaffold } from '../../../../shared/material/scaffold';
13
+ import { Scaffold } from '../../../../shared/material/scaffold';
14
14
  import { MyPageContainer } from '../../../../shared/widgets/my_page_container';
15
15
  import type { ContactPI } from '../../../contacts/domain/contact_pi';
16
16
  import type { PhoneContact } from '../../../contacts/domain/phone_contact';
17
17
  import { ContactBloc } from '../../../contacts/presentation/bloc/contact_bloc';
18
18
  import { ContactSearchEvent } from '../../../contacts/presentation/bloc/contact_event';
19
19
  import { ContactAliasListWidget } from '../../../contacts/presentation/pages/contact_list_alias_widget';
20
+ import { ProfileAppBar } from './profile_app_bar';
20
21
  import { ProfilePageContactsActions } from './profile_page_contacts_actions';
21
22
 
22
23
  /**
@@ -42,11 +43,10 @@ export function ProfilePageContacts({ contactPI }: { contactPI?: ContactPI | nul
42
43
  };
43
44
 
44
45
  return (
45
- <Scaffold appBar={<AppBar onBack={() => AppRouter.pop(router)} />}>
46
+ <Scaffold appBar={<ProfileAppBar titre={traductions.profileContactPageTitle} />}>
46
47
  <MyPageContainer>
47
48
  <Pressable style={styles.flex} onPress={Keyboard.dismiss} accessible={false}>
48
- <Text style={theme.textTheme.titleSmall}>{traductions.profileContactPageTitle}</Text>
49
- <View style={styles.gap10} />
49
+ <View style={styles.gap8} />
50
50
  {/* Recherche d'un contact */}
51
51
  <Card>
52
52
  <View style={[styles.search, { backgroundColor: theme.inputDecorationTheme.fillColor }]}>
@@ -99,6 +99,7 @@ export function ProfilePageContacts({ contactPI }: { contactPI?: ContactPI | nul
99
99
  }
100
100
 
101
101
  const styles = StyleSheet.create({
102
+ gap8: { height: 8 },
102
103
  flex: { flex: 1 },
103
104
  search: { height: 36, flexDirection: 'row', alignItems: 'center', paddingHorizontal: 12 },
104
105
  input: { flex: 1, marginLeft: 8, paddingVertical: 0 },
@@ -1,6 +1,8 @@
1
1
  import { Platform } from 'react-native';
2
2
 
3
3
  import { Images } from '../../../../core/assets';
4
+ import { Di } from '../../../../core/di';
5
+ import { AppEnv } from '../../../../core/env';
4
6
  import { AppRouter, type SdkRouter } from '../../../../core/router';
5
7
  import type { AppLocalizations } from '../../../../l10n/app_localizations';
6
8
  import { Fonctionnalite } from '../../domain/models/fonctionnalite';
@@ -65,6 +67,13 @@ export function fonctionnalitesRecherchables(
65
67
  motsCles: ['historique', 'paiement', 'operation', 'releve', 'history', 'payment', 'historico', 'pagamento'],
66
68
  ouvrir: () => aller(AppRouter.transactionSearch),
67
69
  }),
70
+ new Fonctionnalite({
71
+ icon: Images.iconsCloche,
72
+ titre: traductions.searchNotificationsTitle,
73
+ sousTitre: traductions.searchNotificationsSubTitle,
74
+ motsCles: ['notification', 'alerte', 'message', 'nouveau', 'alert', 'news', 'notificacao', 'alerta'],
75
+ ouvrir: () => aller(AppRouter.notifications),
76
+ }),
68
77
 
69
78
  // Abonnements
70
79
  new Fonctionnalite({
@@ -111,6 +120,25 @@ export function fonctionnalitesRecherchables(
111
120
  motsCles: ['parametre', 'reglage', 'theme', 'langue', 'notification', 'sombre', 'settings', 'language', 'dark', 'definicoes', 'idioma'],
112
121
  ouvrir: () => aller(AppRouter.profileParametre),
113
122
  }),
123
+ // Masqué si l'hôte n'autorise pas le client à changer de thème
124
+ ...(Di.config.appConfig.canChangeTheme
125
+ ? [
126
+ new Fonctionnalite({
127
+ icon: Images.iconsTheme,
128
+ titre: traductions.searchThemeTitle,
129
+ sousTitre: traductions.searchThemeSubTitle,
130
+ motsCles: ['theme', 'apparence', 'sombre', 'clair', 'couleur', 'mode', 'appearance', 'dark', 'light', 'color', 'tema', 'aparencia', 'escuro', 'claro', 'cor'],
131
+ ouvrir: () => aller(AppRouter.profileParametreTheme),
132
+ }),
133
+ ]
134
+ : []),
135
+ new Fonctionnalite({
136
+ icon: Images.iconsCloche,
137
+ titre: traductions.searchNotificationsParametreTitle,
138
+ sousTitre: traductions.searchNotificationsParametreSubTitle,
139
+ motsCles: ['notification', 'son', 'vibration', 'sonnerie', 'style', 'sound', 'vibrate', 'som', 'vibracao', 'estilo'],
140
+ ouvrir: () => aller(AppRouter.profileParametreNotifications),
141
+ }),
114
142
  // Sur le web, le profil n'a pas ces pages : pas de contacts,
115
143
  // et l'aide est un lien externe.
116
144
  ...(web
@@ -123,13 +151,18 @@ export function fonctionnalitesRecherchables(
123
151
  motsCles: ['contact', 'alias', 'repertoire', 'contacts', 'contactos'],
124
152
  ouvrir: () => aller(AppRouter.profileContacts),
125
153
  }),
126
- new Fonctionnalite({
127
- icon: Images.iconsHelp,
128
- titre: traductions.profilePageMenuHelpTitle,
129
- sousTitre: traductions.searchAideSubTitle,
130
- motsCles: ['aide', 'faq', 'assistance', 'support', 'help', 'ajuda'],
131
- ouvrir: () => aller(AppRouter.profileHelpCenter),
132
- }),
154
+ // Seulement si l'hôte fournit un lien d'aide, comme dans le profil
155
+ ...((AppEnv.aideLink?.length ?? 0) > 0
156
+ ? [
157
+ new Fonctionnalite({
158
+ icon: Images.iconsHelp,
159
+ titre: traductions.profilePageMenuHelpTitle,
160
+ sousTitre: traductions.searchAideSubTitle,
161
+ motsCles: ['aide', 'faq', 'assistance', 'support', 'help', 'ajuda'],
162
+ ouvrir: () => aller(AppRouter.profileHelpCenter),
163
+ }),
164
+ ]
165
+ : []),
133
166
  ]),
134
167
  ];
135
168
  }
@@ -289,7 +289,7 @@ export function SearchPage({
289
289
  </Text>
290
290
  </View>
291
291
  ) : (
292
- <ScrollView keyboardDismissMode="on-drag" keyboardShouldPersistTaps="handled" contentContainerStyle={styles.liste}>
292
+ <ScrollView keyboardShouldPersistTaps="handled" contentContainerStyle={styles.liste}>
293
293
  <MenuActionsWidget items={resultats.map(ligne)} />
294
294
  </ScrollView>
295
295
  )}
@@ -17,6 +17,7 @@ import { DateFormat } from '../../../../../shared/utils/date_format';
17
17
  import { AliasWidget } from '../../../../../shared/widgets/alias_widget';
18
18
  import { formatMontant } from '../../../../../shared/widgets/amount_widget';
19
19
  import { ClipboardWidget } from '../../../../../shared/widgets/clipboard_widget';
20
+ import { ErrorCardWidget } from '../../../../../shared/widgets/error_card_widget';
20
21
  import { ErrorPage, ErrorType } from '../../../../../shared/widgets/error_page';
21
22
  import { CustomLoadingDialog, LoadingPage } from '../../../../../shared/widgets/loading_page';
22
23
  import { NotificationDialog, NotificationType } from '../../../../../shared/widgets/notification_dialog';
@@ -260,9 +261,7 @@ export function TransactionRtpPage({
260
261
  {toPay && !canDoAction() && (
261
262
  <>
262
263
  <View style={styles.gap16} />
263
- <Text style={[theme.textTheme.bodySmall, styles.warning, { color: Themer.systemErrorColor }]}>
264
- {traductions.transactionRtpWarningMessage}
265
- </Text>
264
+ <ErrorCardWidget icon="shield" message={traductions.transactionRtpWarningMessage} />
266
265
  </>
267
266
  )}
268
267
  </ScrollView>
@@ -487,7 +486,6 @@ const styles = StyleSheet.create({
487
486
  row: { flexDirection: 'row', alignItems: 'center' },
488
487
  value: { flex: 1, paddingLeft: 10 },
489
488
  right: { textAlign: 'right' },
490
- warning: { marginLeft: 8, marginRight: 20, marginTop: 2, marginBottom: 5 },
491
489
  gap3: { height: 3 },
492
490
  gap10: { height: 10 },
493
491
  gap16: { height: 16 },
@@ -1,5 +1,6 @@
1
1
  import { MaterialIcons } from '@expo/vector-icons';
2
- import { Pressable, StyleSheet, Text, View } from 'react-native';
2
+ import type { ComponentProps } from 'react';
3
+ import { Pressable, StyleSheet, View } from 'react-native';
3
4
 
4
5
  import { BlocProvider, useBloc } from '../../../../../core/bloc_react';
5
6
  import { Di } from '../../../../../core/di';
@@ -9,6 +10,7 @@ import { useAppLocalizations } from '../../../../../l10n/localizations';
9
10
  import { ElevatedButton, FilledTonalButton } from '../../../../../shared/material/buttons';
10
11
  import { showModalBottomSheet } from '../../../../../shared/material/overlays';
11
12
  import { FrequenceCommand } from '../../../../../shared/models/frequence_command';
13
+ import { ErrorCardWidget } from '../../../../../shared/widgets/error_card_widget';
12
14
  import type { Subscription } from '../../../../subscription/domain/models/subscription';
13
15
  import { TransactionStatut, type Transaction } from '../../../domain/models/transaction';
14
16
  import { TransactionSendCommand } from '../../../domain/models/transaction_send/transaction_send_command';
@@ -80,18 +82,23 @@ export function TransactionRtpPageActions({
80
82
  ),
81
83
  });
82
84
 
83
- const warning = (message: string) => (
84
- <Text style={[theme.inputDecorationTheme.errorStyle, styles.warning]}>{`⚠ ${message}`}</Text>
85
+ const warning = (message: string, icon: ComponentProps<typeof ErrorCardWidget>['icon']) => (
86
+ <ErrorCardWidget message={message} icon={icon} />
85
87
  );
86
88
 
87
89
  if (isExpired(tx)) {
88
- return <View style={styles.padding}>{warning(traductions.transactionRtpExpiredWarningMessage)}</View>;
90
+ return <View style={styles.padding}>{warning(traductions.transactionRtpExpiredWarningMessage, 'timer-off')}</View>;
89
91
  }
90
92
 
91
93
  const scheduleActive = canDoAction && sub == null;
92
94
  return (
93
95
  <View style={styles.padding}>
94
- {isSoldeInsuffisant && warning(traductions.transactionFormAmountInvalid)}
96
+ {isSoldeInsuffisant && (
97
+ <>
98
+ {warning(traductions.transactionFormAmountInvalid, 'account-balance-wallet')}
99
+ <View style={styles.gap12} />
100
+ </>
101
+ )}
95
102
  <View style={styles.row}>
96
103
  {tx.canSchedule() && sub == null && (
97
104
  <>
@@ -128,7 +135,7 @@ export function TransactionRtpPageActions({
128
135
 
129
136
  const styles = StyleSheet.create({
130
137
  padding: { paddingVertical: 10, paddingHorizontal: 10 },
131
- warning: { marginLeft: 8, marginTop: 2, marginBottom: 5 },
138
+ gap12: { height: 12 },
132
139
  row: { flexDirection: 'row', alignItems: 'center' },
133
140
  flex: { flex: 1 },
134
141
  button: { height: 56 },
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Équivalent de `Color.withValues(alpha:)` : la couleur [color] avec
3
+ * l'opacité [alpha] (0 à 1). Accepte `#RGB`, `#RRGGBB` et `#RRGGBBAA` ;
4
+ * toute autre forme est rendue telle quelle.
5
+ */
6
+ export function withAlpha(color: string, alpha: number): string {
7
+ let hex = /^#([0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i.exec(color)?.[1];
8
+ if (hex == null) return color;
9
+ if (hex.length === 3) hex = hex.replace(/./g, (c) => c + c);
10
+ const a = Math.round(Math.min(1, Math.max(0, alpha)) * 255)
11
+ .toString(16)
12
+ .padStart(2, '0');
13
+ return `#${hex.slice(0, 6)}${a}`;
14
+ }
@@ -0,0 +1,58 @@
1
+ import { MaterialIcons } from '@expo/vector-icons';
2
+ import type { ComponentProps } from 'react';
3
+ import { StyleSheet, Text, View } from 'react-native';
4
+
5
+ import { Themer, useTheme } from '../../core/theme';
6
+ import { Card } from '../material/card';
7
+
8
+ /** Couleur d'erreur du thème avec une opacité (0 à 1). */
9
+ function erreur(alpha: number): string {
10
+ const hex = Math.round(alpha * 255).toString(16).padStart(2, '0');
11
+ return `${Themer.systemErrorColor}${hex}`;
12
+ }
13
+
14
+ /**
15
+ * Carte d'avertissement : fond teinté, bordure et icône dans la couleur
16
+ * d'erreur du thème, suivies du `message`.
17
+ */
18
+ export function ErrorCardWidget({
19
+ message,
20
+ icon = 'warning-amber',
21
+ }: {
22
+ message: string;
23
+ icon?: ComponentProps<typeof MaterialIcons>['name'];
24
+ }) {
25
+ const theme = useTheme();
26
+ const texte = theme.textTheme.bodySmall;
27
+ return (
28
+ <Card style={styles.card}>
29
+ <View style={styles.row}>
30
+ <View style={styles.icon}>
31
+ <MaterialIcons name={icon} size={18} color={Themer.systemErrorColor} />
32
+ </View>
33
+ <Text
34
+ style={[
35
+ texte,
36
+ styles.message,
37
+ texte?.fontSize != null && { lineHeight: texte.fontSize * 1.4 },
38
+ ]}
39
+ >
40
+ {message}
41
+ </Text>
42
+ </View>
43
+ </Card>
44
+ );
45
+ }
46
+
47
+ const styles = StyleSheet.create({
48
+ card: {
49
+ margin: 0,
50
+ backgroundColor: erreur(0.08),
51
+ borderRadius: 16,
52
+ borderWidth: 1,
53
+ borderColor: erreur(0.35),
54
+ },
55
+ row: { flexDirection: 'row', alignItems: 'flex-start', padding: 14 },
56
+ icon: { padding: 6, borderRadius: 999, backgroundColor: erreur(0.15), marginRight: 12 },
57
+ message: { flex: 1, textAlign: 'left', color: Themer.systemErrorColor },
58
+ });
@@ -0,0 +1,98 @@
1
+ import { MaterialIcons } from '@expo/vector-icons';
2
+ import type { ReactNode } from 'react';
3
+ import { Pressable, StyleSheet, Text, useWindowDimensions, View } from 'react-native';
4
+ import { useSafeAreaInsets } from 'react-native-safe-area-context';
5
+
6
+ import { Themer, useTheme } from '../../core/theme';
7
+ import { withAlpha } from '../utils/color';
8
+
9
+ /**
10
+ * Feuille de sélection : poignée, [titre], [entete] facultatif, puis le
11
+ * [contenu] (qui défile s'il est trop long : utiliser un ScrollView).
12
+ * À ouvrir avec showModalBottomSheet.
13
+ */
14
+ export function SbSelectionSheet({
15
+ titre,
16
+ titreSize,
17
+ entete,
18
+ contenu,
19
+ }: {
20
+ titre?: string;
21
+ titreSize?: number;
22
+ entete?: ReactNode;
23
+ contenu: ReactNode;
24
+ }) {
25
+ const theme = useTheme();
26
+ const insets = useSafeAreaInsets();
27
+ const { height } = useWindowDimensions();
28
+ return (
29
+ <View
30
+ style={[
31
+ styles.sheet,
32
+ { maxHeight: height * 0.75, backgroundColor: theme.colorScheme.surface, paddingBottom: insets.bottom },
33
+ ]}
34
+ >
35
+ {/* Poignée */}
36
+ <View style={[styles.poignee, { backgroundColor: withAlpha(Themer.neural03Color, 0.4) }]} />
37
+ {titre != null && (
38
+ <Text style={[theme.textTheme.headlineLarge, styles.titre, titreSize != null ? { fontSize: titreSize } : null]}>{titre}</Text>
39
+ )}
40
+ {entete != null && <View style={styles.entete}>{entete}</View>}
41
+ <View style={styles.gap8} />
42
+ <View style={styles.contenu}>{contenu}</View>
43
+ </View>
44
+ );
45
+ }
46
+
47
+ /** Ligne d'une feuille de sélection : [pastille], [texte], [detail] facultatif et coche si elle est [choisie]. */
48
+ export function SbSelectionLigne({
49
+ pastille,
50
+ texte,
51
+ detail,
52
+ choisie,
53
+ onPress,
54
+ }: {
55
+ pastille: ReactNode;
56
+ texte: string;
57
+ detail?: string;
58
+ choisie: boolean;
59
+ onPress: () => void;
60
+ }) {
61
+ const theme = useTheme();
62
+ const primaire = theme.colorScheme.primary;
63
+ return (
64
+ <Pressable
65
+ accessibilityRole="button"
66
+ accessibilityState={{ selected: choisie }}
67
+ onPress={onPress}
68
+ style={({ pressed }) => [
69
+ styles.ligne,
70
+ { backgroundColor: choisie ? withAlpha(primaire, 0.08) : pressed ? theme.colorScheme.secondary : 'transparent' },
71
+ ]}
72
+ >
73
+ {pastille}
74
+ <View style={styles.textes}>
75
+ <Text numberOfLines={1} style={[theme.textTheme.displayLarge, { fontWeight: choisie ? '700' : '500' }]}>
76
+ {texte}
77
+ </Text>
78
+ {detail != null && detail.length > 0 && (
79
+ <Text numberOfLines={1} style={[theme.textTheme.displaySmall, { color: Themer.neural03Color }]}>
80
+ {detail}
81
+ </Text>
82
+ )}
83
+ </View>
84
+ {choisie && <MaterialIcons name="check-circle" size={22} color={primaire} />}
85
+ </Pressable>
86
+ );
87
+ }
88
+
89
+ const styles = StyleSheet.create({
90
+ sheet: { width: '100%', borderTopLeftRadius: 20, borderTopRightRadius: 20 },
91
+ poignee: { alignSelf: 'center', marginTop: 10, marginBottom: 14, width: 36, height: 4, borderRadius: 2 },
92
+ titre: { paddingHorizontal: 20 },
93
+ entete: { paddingHorizontal: 20, paddingTop: 12 },
94
+ contenu: { flexShrink: 1 },
95
+ gap8: { height: 8 },
96
+ ligne: { flexDirection: 'row', alignItems: 'center', marginVertical: 2, paddingHorizontal: 8, paddingVertical: 10, borderRadius: 14 },
97
+ textes: { flex: 1, marginHorizontal: 14 },
98
+ });