@pi-spi/app 1.5.0 → 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 (117) hide show
  1. package/README.md +3 -0
  2. package/lib/module/core/api.js +9 -0
  3. package/lib/module/core/api.js.map +1 -1
  4. package/lib/module/core/pi_spi_sdk_config.js +10 -0
  5. package/lib/module/core/pi_spi_sdk_config.js.map +1 -1
  6. package/lib/module/l10n/app_localizations.js.map +1 -1
  7. package/lib/module/l10n/app_localizations_en.js +12 -1
  8. package/lib/module/l10n/app_localizations_en.js.map +1 -1
  9. package/lib/module/l10n/app_localizations_fr.js +12 -1
  10. package/lib/module/l10n/app_localizations_fr.js.map +1 -1
  11. package/lib/module/l10n/app_localizations_pt.js +12 -1
  12. package/lib/module/l10n/app_localizations_pt.js.map +1 -1
  13. package/lib/module/modules/alias/presentation/pages/alias_delete_btn_widget.js +15 -19
  14. package/lib/module/modules/alias/presentation/pages/alias_delete_btn_widget.js.map +1 -1
  15. package/lib/module/modules/alias/presentation/pages/alias_page_claim.js +4 -11
  16. package/lib/module/modules/alias/presentation/pages/alias_page_claim.js.map +1 -1
  17. package/lib/module/modules/compte/presentation/pages/compte_details_widget.js +6 -7
  18. package/lib/module/modules/compte/presentation/pages/compte_details_widget.js.map +1 -1
  19. package/lib/module/modules/profile/presentation/pages/compte/profile_compte_page.js +25 -11
  20. package/lib/module/modules/profile/presentation/pages/compte/profile_compte_page.js.map +1 -1
  21. package/lib/module/modules/profile/presentation/pages/parametre/profile_parametre_page.js +12 -18
  22. package/lib/module/modules/profile/presentation/pages/parametre/profile_parametre_page.js.map +1 -1
  23. package/lib/module/modules/profile/presentation/pages/parametre/profile_parametre_page_notifications.js +109 -103
  24. package/lib/module/modules/profile/presentation/pages/parametre/profile_parametre_page_notifications.js.map +1 -1
  25. package/lib/module/modules/profile/presentation/pages/parametre/profile_parametre_page_notifications_son.js +55 -72
  26. package/lib/module/modules/profile/presentation/pages/parametre/profile_parametre_page_notifications_son.js.map +1 -1
  27. package/lib/module/modules/profile/presentation/pages/parametre/profile_parametre_page_qrcode.js +31 -45
  28. package/lib/module/modules/profile/presentation/pages/parametre/profile_parametre_page_qrcode.js.map +1 -1
  29. package/lib/module/modules/profile/presentation/pages/parametre/profile_parametre_page_theme.js +103 -67
  30. package/lib/module/modules/profile/presentation/pages/parametre/profile_parametre_page_theme.js.map +1 -1
  31. package/lib/module/modules/profile/presentation/pages/profile_app_bar.js +26 -0
  32. package/lib/module/modules/profile/presentation/pages/profile_app_bar.js.map +1 -0
  33. package/lib/module/modules/profile/presentation/pages/profile_menu.js +143 -34
  34. package/lib/module/modules/profile/presentation/pages/profile_menu.js.map +1 -1
  35. package/lib/module/modules/profile/presentation/pages/profile_page.js +95 -63
  36. package/lib/module/modules/profile/presentation/pages/profile_page.js.map +1 -1
  37. package/lib/module/modules/profile/presentation/pages/profile_page_avatar.js +97 -45
  38. package/lib/module/modules/profile/presentation/pages/profile_page_avatar.js.map +1 -1
  39. package/lib/module/modules/profile/presentation/pages/profile_page_contacts.js +9 -8
  40. package/lib/module/modules/profile/presentation/pages/profile_page_contacts.js.map +1 -1
  41. package/lib/module/modules/search/presentation/pages/search_fonctionnalites.js +26 -2
  42. package/lib/module/modules/search/presentation/pages/search_fonctionnalites.js.map +1 -1
  43. package/lib/module/modules/search/presentation/pages/search_page.js +0 -1
  44. package/lib/module/modules/search/presentation/pages/search_page.js.map +1 -1
  45. package/lib/module/modules/transactions/presentation/pages/transaction_rtp/transaction_rtp_page.js +4 -11
  46. package/lib/module/modules/transactions/presentation/pages/transaction_rtp/transaction_rtp_page.js.map +1 -1
  47. package/lib/module/modules/transactions/presentation/pages/transaction_rtp/transaction_rtp_page_actions.js +13 -10
  48. package/lib/module/modules/transactions/presentation/pages/transaction_rtp/transaction_rtp_page_actions.js.map +1 -1
  49. package/lib/module/shared/utils/color.js +15 -0
  50. package/lib/module/shared/utils/color.js.map +1 -0
  51. package/lib/module/shared/widgets/error_card_widget.js +70 -0
  52. package/lib/module/shared/widgets/error_card_widget.js.map +1 -0
  53. package/lib/module/shared/widgets/selection.js +136 -0
  54. package/lib/module/shared/widgets/selection.js.map +1 -0
  55. package/lib/typescript/src/core/api.d.ts.map +1 -1
  56. package/lib/typescript/src/core/pi_spi_sdk_config.d.ts +10 -0
  57. package/lib/typescript/src/core/pi_spi_sdk_config.d.ts.map +1 -1
  58. package/lib/typescript/src/l10n/app_localizations.d.ts +22 -0
  59. package/lib/typescript/src/l10n/app_localizations.d.ts.map +1 -1
  60. package/lib/typescript/src/l10n/app_localizations_en.d.ts.map +1 -1
  61. package/lib/typescript/src/l10n/app_localizations_fr.d.ts.map +1 -1
  62. package/lib/typescript/src/l10n/app_localizations_pt.d.ts.map +1 -1
  63. package/lib/typescript/src/modules/alias/presentation/pages/alias_delete_btn_widget.d.ts.map +1 -1
  64. package/lib/typescript/src/modules/alias/presentation/pages/alias_page_claim.d.ts.map +1 -1
  65. package/lib/typescript/src/modules/compte/presentation/pages/compte_details_widget.d.ts.map +1 -1
  66. package/lib/typescript/src/modules/profile/presentation/pages/compte/profile_compte_page.d.ts.map +1 -1
  67. package/lib/typescript/src/modules/profile/presentation/pages/parametre/profile_parametre_page.d.ts.map +1 -1
  68. package/lib/typescript/src/modules/profile/presentation/pages/parametre/profile_parametre_page_notifications.d.ts.map +1 -1
  69. package/lib/typescript/src/modules/profile/presentation/pages/parametre/profile_parametre_page_notifications_son.d.ts.map +1 -1
  70. package/lib/typescript/src/modules/profile/presentation/pages/parametre/profile_parametre_page_qrcode.d.ts.map +1 -1
  71. package/lib/typescript/src/modules/profile/presentation/pages/parametre/profile_parametre_page_theme.d.ts.map +1 -1
  72. package/lib/typescript/src/modules/profile/presentation/pages/profile_app_bar.d.ts +5 -0
  73. package/lib/typescript/src/modules/profile/presentation/pages/profile_app_bar.d.ts.map +1 -0
  74. package/lib/typescript/src/modules/profile/presentation/pages/profile_menu.d.ts +31 -4
  75. package/lib/typescript/src/modules/profile/presentation/pages/profile_menu.d.ts.map +1 -1
  76. package/lib/typescript/src/modules/profile/presentation/pages/profile_page.d.ts +4 -1
  77. package/lib/typescript/src/modules/profile/presentation/pages/profile_page.d.ts.map +1 -1
  78. package/lib/typescript/src/modules/profile/presentation/pages/profile_page_avatar.d.ts +1 -1
  79. package/lib/typescript/src/modules/profile/presentation/pages/profile_page_avatar.d.ts.map +1 -1
  80. package/lib/typescript/src/modules/profile/presentation/pages/profile_page_contacts.d.ts.map +1 -1
  81. package/lib/typescript/src/modules/search/presentation/pages/search_fonctionnalites.d.ts.map +1 -1
  82. package/lib/typescript/src/modules/transactions/presentation/pages/transaction_rtp/transaction_rtp_page.d.ts.map +1 -1
  83. package/lib/typescript/src/modules/transactions/presentation/pages/transaction_rtp/transaction_rtp_page_actions.d.ts.map +1 -1
  84. package/lib/typescript/src/shared/utils/color.d.ts +7 -0
  85. package/lib/typescript/src/shared/utils/color.d.ts.map +1 -0
  86. package/lib/typescript/src/shared/widgets/error_card_widget.d.ts +11 -0
  87. package/lib/typescript/src/shared/widgets/error_card_widget.d.ts.map +1 -0
  88. package/lib/typescript/src/shared/widgets/selection.d.ts +21 -0
  89. package/lib/typescript/src/shared/widgets/selection.d.ts.map +1 -0
  90. package/package.json +1 -1
  91. package/src/core/api.ts +4 -0
  92. package/src/core/pi_spi_sdk_config.ts +14 -0
  93. package/src/l10n/app_localizations.ts +22 -0
  94. package/src/l10n/app_localizations_en.ts +11 -0
  95. package/src/l10n/app_localizations_fr.ts +11 -0
  96. package/src/l10n/app_localizations_pt.ts +11 -0
  97. package/src/modules/alias/presentation/pages/alias_delete_btn_widget.tsx +13 -12
  98. package/src/modules/alias/presentation/pages/alias_page_claim.tsx +5 -4
  99. package/src/modules/compte/presentation/pages/compte_details_widget.tsx +3 -4
  100. package/src/modules/profile/presentation/pages/compte/profile_compte_page.tsx +15 -7
  101. package/src/modules/profile/presentation/pages/parametre/profile_parametre_page.tsx +10 -12
  102. package/src/modules/profile/presentation/pages/parametre/profile_parametre_page_notifications.tsx +89 -68
  103. package/src/modules/profile/presentation/pages/parametre/profile_parametre_page_notifications_son.tsx +84 -54
  104. package/src/modules/profile/presentation/pages/parametre/profile_parametre_page_qrcode.tsx +28 -24
  105. package/src/modules/profile/presentation/pages/parametre/profile_parametre_page_theme.tsx +77 -44
  106. package/src/modules/profile/presentation/pages/profile_app_bar.tsx +21 -0
  107. package/src/modules/profile/presentation/pages/profile_menu.tsx +126 -33
  108. package/src/modules/profile/presentation/pages/profile_page.tsx +90 -66
  109. package/src/modules/profile/presentation/pages/profile_page_avatar.tsx +69 -28
  110. package/src/modules/profile/presentation/pages/profile_page_contacts.tsx +5 -4
  111. package/src/modules/search/presentation/pages/search_fonctionnalites.ts +40 -7
  112. package/src/modules/search/presentation/pages/search_page.tsx +1 -1
  113. package/src/modules/transactions/presentation/pages/transaction_rtp/transaction_rtp_page.tsx +2 -4
  114. package/src/modules/transactions/presentation/pages/transaction_rtp/transaction_rtp_page_actions.tsx +13 -6
  115. package/src/shared/utils/color.ts +14 -0
  116. package/src/shared/widgets/error_card_widget.tsx +58 -0
  117. package/src/shared/widgets/selection.tsx +98 -0
@@ -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
  });
@@ -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
+ });