@pi-spi/app 1.4.0 → 1.5.1

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 (94) hide show
  1. package/README.md +49 -2
  2. package/example/App.tsx +16 -1
  3. package/lib/module/core/api.js +9 -0
  4. package/lib/module/core/api.js.map +1 -1
  5. package/lib/module/core/di.js +8 -1
  6. package/lib/module/core/di.js.map +1 -1
  7. package/lib/module/core/pi_spi_custom_theme.js +63 -0
  8. package/lib/module/core/pi_spi_custom_theme.js.map +1 -0
  9. package/lib/module/core/pi_spi_sdk_config.js +25 -3
  10. package/lib/module/core/pi_spi_sdk_config.js.map +1 -1
  11. package/lib/module/core/theme.js +51 -0
  12. package/lib/module/core/theme.js.map +1 -1
  13. package/lib/module/index.js +1 -0
  14. package/lib/module/index.js.map +1 -1
  15. package/lib/module/l10n/app_localizations.js.map +1 -1
  16. package/lib/module/l10n/app_localizations_en.js +3 -0
  17. package/lib/module/l10n/app_localizations_en.js.map +1 -1
  18. package/lib/module/l10n/app_localizations_fr.js +3 -0
  19. package/lib/module/l10n/app_localizations_fr.js.map +1 -1
  20. package/lib/module/l10n/app_localizations_pt.js +3 -0
  21. package/lib/module/l10n/app_localizations_pt.js.map +1 -1
  22. package/lib/module/modules/home/presentation/pages/home_tab_compte.js +1 -2
  23. package/lib/module/modules/home/presentation/pages/home_tab_compte.js.map +1 -1
  24. package/lib/module/modules/introduction/presentation/pages/introduction_bar.js +8 -1
  25. package/lib/module/modules/introduction/presentation/pages/introduction_bar.js.map +1 -1
  26. package/lib/module/modules/introduction/presentation/pages/introduction_page.js +29 -4
  27. package/lib/module/modules/introduction/presentation/pages/introduction_page.js.map +1 -1
  28. package/lib/module/modules/profile/presentation/pages/parametre/profile_parametre_page.js +2 -2
  29. package/lib/module/modules/profile/presentation/pages/parametre/profile_parametre_page.js.map +1 -1
  30. package/lib/module/modules/profile/presentation/pages/parametre/profile_parametre_page_theme.js +156 -25
  31. package/lib/module/modules/profile/presentation/pages/parametre/profile_parametre_page_theme.js.map +1 -1
  32. package/lib/module/modules/transactions/domain/models/transaction_send/transaction_send_command.js +6 -2
  33. package/lib/module/modules/transactions/domain/models/transaction_send/transaction_send_command.js.map +1 -1
  34. package/lib/module/modules/transactions/presentation/pages/transaction_form/transaction_form_input_motif.js +3 -1
  35. package/lib/module/modules/transactions/presentation/pages/transaction_form/transaction_form_input_motif.js.map +1 -1
  36. package/lib/module/modules/transactions/presentation/pages/transaction_jour_libelle.js +15 -0
  37. package/lib/module/modules/transactions/presentation/pages/transaction_jour_libelle.js.map +1 -0
  38. package/lib/module/modules/transactions/presentation/pages/transaction_list_item_widget.js +2 -1
  39. package/lib/module/modules/transactions/presentation/pages/transaction_list_item_widget.js.map +1 -1
  40. package/lib/module/modules/transactions/presentation/pages/transaction_recents/transaction_recents_widget.js +5 -3
  41. package/lib/module/modules/transactions/presentation/pages/transaction_recents/transaction_recents_widget.js.map +1 -1
  42. package/lib/module/modules/transactions/presentation/pages/transaction_search/transaction_search_page_liste.js +3 -2
  43. package/lib/module/modules/transactions/presentation/pages/transaction_search/transaction_search_page_liste.js.map +1 -1
  44. package/lib/typescript/src/core/api.d.ts.map +1 -1
  45. package/lib/typescript/src/core/di.d.ts.map +1 -1
  46. package/lib/typescript/src/core/pi_spi_custom_theme.d.ts +49 -0
  47. package/lib/typescript/src/core/pi_spi_custom_theme.d.ts.map +1 -0
  48. package/lib/typescript/src/core/pi_spi_sdk_config.d.ts +25 -2
  49. package/lib/typescript/src/core/pi_spi_sdk_config.d.ts.map +1 -1
  50. package/lib/typescript/src/core/theme.d.ts +19 -0
  51. package/lib/typescript/src/core/theme.d.ts.map +1 -1
  52. package/lib/typescript/src/index.d.ts +2 -1
  53. package/lib/typescript/src/index.d.ts.map +1 -1
  54. package/lib/typescript/src/l10n/app_localizations.d.ts +6 -0
  55. package/lib/typescript/src/l10n/app_localizations.d.ts.map +1 -1
  56. package/lib/typescript/src/l10n/app_localizations_en.d.ts.map +1 -1
  57. package/lib/typescript/src/l10n/app_localizations_fr.d.ts.map +1 -1
  58. package/lib/typescript/src/l10n/app_localizations_pt.d.ts.map +1 -1
  59. package/lib/typescript/src/modules/introduction/presentation/pages/introduction_bar.d.ts.map +1 -1
  60. package/lib/typescript/src/modules/introduction/presentation/pages/introduction_page.d.ts.map +1 -1
  61. package/lib/typescript/src/modules/profile/presentation/pages/parametre/profile_parametre_page.d.ts.map +1 -1
  62. package/lib/typescript/src/modules/profile/presentation/pages/parametre/profile_parametre_page_theme.d.ts.map +1 -1
  63. package/lib/typescript/src/modules/transactions/domain/models/transaction_send/transaction_send_command.d.ts +1 -1
  64. package/lib/typescript/src/modules/transactions/domain/models/transaction_send/transaction_send_command.d.ts.map +1 -1
  65. package/lib/typescript/src/modules/transactions/presentation/pages/transaction_form/transaction_form_input_motif.d.ts +2 -1
  66. package/lib/typescript/src/modules/transactions/presentation/pages/transaction_form/transaction_form_input_motif.d.ts.map +1 -1
  67. package/lib/typescript/src/modules/transactions/presentation/pages/transaction_jour_libelle.d.ts +4 -0
  68. package/lib/typescript/src/modules/transactions/presentation/pages/transaction_jour_libelle.d.ts.map +1 -0
  69. package/lib/typescript/src/modules/transactions/presentation/pages/transaction_list_item_widget.d.ts +2 -1
  70. package/lib/typescript/src/modules/transactions/presentation/pages/transaction_list_item_widget.d.ts.map +1 -1
  71. package/lib/typescript/src/modules/transactions/presentation/pages/transaction_recents/transaction_recents_widget.d.ts.map +1 -1
  72. package/lib/typescript/src/modules/transactions/presentation/pages/transaction_search/transaction_search_page_liste.d.ts.map +1 -1
  73. package/package.json +1 -1
  74. package/src/core/api.ts +4 -0
  75. package/src/core/di.ts +8 -1
  76. package/src/core/pi_spi_custom_theme.ts +66 -0
  77. package/src/core/pi_spi_sdk_config.ts +32 -3
  78. package/src/core/theme.ts +53 -0
  79. package/src/index.ts +2 -1
  80. package/src/l10n/app_localizations.ts +6 -0
  81. package/src/l10n/app_localizations_en.ts +3 -0
  82. package/src/l10n/app_localizations_fr.ts +3 -0
  83. package/src/l10n/app_localizations_pt.ts +3 -0
  84. package/src/modules/home/presentation/pages/home_tab_compte.tsx +1 -1
  85. package/src/modules/introduction/presentation/pages/introduction_bar.tsx +12 -2
  86. package/src/modules/introduction/presentation/pages/introduction_page.tsx +20 -2
  87. package/src/modules/profile/presentation/pages/parametre/profile_parametre_page.tsx +5 -2
  88. package/src/modules/profile/presentation/pages/parametre/profile_parametre_page_theme.tsx +95 -15
  89. package/src/modules/transactions/domain/models/transaction_send/transaction_send_command.ts +6 -2
  90. package/src/modules/transactions/presentation/pages/transaction_form/transaction_form_input_motif.tsx +3 -0
  91. package/src/modules/transactions/presentation/pages/transaction_jour_libelle.ts +17 -0
  92. package/src/modules/transactions/presentation/pages/transaction_list_item_widget.tsx +3 -1
  93. package/src/modules/transactions/presentation/pages/transaction_recents/transaction_recents_widget.tsx +7 -4
  94. package/src/modules/transactions/presentation/pages/transaction_search/transaction_search_page_liste.tsx +3 -2
@@ -0,0 +1,66 @@
1
+ import { Themer, lerpColor, type ThemeData } from './theme';
2
+
3
+ /**
4
+ * Thème personnalisé de l'application hôte, quand aucun des thèmes du SDK
5
+ * (`Themes`) ne convient. À passer dans `PiSpiAppConfig.customTheme`.
6
+ *
7
+ * Deux façons de le définir :
8
+ *
9
+ * - **Par ses couleurs** (cas courant) : le SDK construit un thème complet
10
+ * sur le même modèle que ses thèmes clairs colorés (rouge, violet,
11
+ * rose...). Seule `primary` est obligatoire ; les autres couleurs en sont
12
+ * dérivées si elles ne sont pas fournies.
13
+ *
14
+ * ```ts
15
+ * new PiSpiCustomTheme({ primary: '#00695C', secondary: '#B2DFDB' })
16
+ * ```
17
+ *
18
+ * - **Par un `ThemeData` complet** (avancé), avec
19
+ * `PiSpiCustomTheme.themeData`. Partez de préférence du thème construit
20
+ * par le SDK, pour conserver tous les réglages dont ses écrans ont
21
+ * besoin (cartes, champs, boutons, textes) :
22
+ *
23
+ * ```ts
24
+ * const base = new PiSpiCustomTheme({ primary: '#00695C' }).build();
25
+ * PiSpiCustomTheme.themeData({ ...base, cardColor: '#F1F8F7' })
26
+ * ```
27
+ */
28
+ export class PiSpiCustomTheme {
29
+ /** Couleur principale (`#RRGGBB`) : boutons, liens, icônes, champ actif. */
30
+ readonly primary?: string;
31
+ /** Couleur claire : avatars, puces de filtre, fonds secondaires. Par défaut, `primary` éclaircie à 75 %. */
32
+ readonly secondary?: string;
33
+ /** Couleur intermédiaire (tertiaire du schéma de couleurs). Par défaut, `primary` éclaircie à 40 %. */
34
+ readonly tertiary?: string;
35
+ /** Couleur d'un bouton appuyé. Par défaut, `primary` assombrie à 20 %. */
36
+ readonly pressed?: string;
37
+
38
+ private themeDataValue?: ThemeData;
39
+
40
+ /** Thème construit à partir de ses couleurs. */
41
+ constructor(params: { primary: string; secondary?: string; tertiary?: string; pressed?: string }) {
42
+ this.primary = params.primary;
43
+ this.secondary = params.secondary;
44
+ this.tertiary = params.tertiary;
45
+ this.pressed = params.pressed;
46
+ }
47
+
48
+ /** Thème fourni entièrement par l'hôte (usage avancé, voir la documentation de la classe). */
49
+ static themeData(theme: ThemeData): PiSpiCustomTheme {
50
+ const custom = new PiSpiCustomTheme({ primary: theme.primaryColor });
51
+ custom.themeDataValue = theme;
52
+ return custom;
53
+ }
54
+
55
+ /** Le `ThemeData` appliqué aux écrans du SDK. */
56
+ build(): ThemeData {
57
+ if (this.themeDataValue != null) return this.themeDataValue;
58
+ const main = this.primary!;
59
+ return Themer.colored(
60
+ main,
61
+ this.secondary ?? lerpColor(main, '#FFFFFF', 0.75),
62
+ this.tertiary ?? lerpColor(main, '#FFFFFF', 0.4),
63
+ this.pressed ?? lerpColor(main, '#000000', 0.2),
64
+ );
65
+ }
66
+ }
@@ -1,4 +1,5 @@
1
1
  import { supportedLocales, type SupportedLocaleCode } from '../l10n/app_localizations';
2
+ import type { PiSpiCustomTheme } from './pi_spi_custom_theme';
2
3
  import { Themes } from './theme';
3
4
 
4
5
  /**
@@ -155,18 +156,32 @@ export class PiSpiApiConfig {
155
156
  */
156
157
  readonly demoMode: boolean;
157
158
 
159
+ /**
160
+ * Clé d'API optionnelle, envoyée dans l'en-tête {@link apiKeyHeader} de
161
+ * chaque requête (en plus du token d'accès), quand la passerelle du
162
+ * participant en exige une. `undefined` : aucun en-tête n'est ajouté.
163
+ */
164
+ readonly apiKey?: string;
165
+
166
+ /** Nom de l'en-tête HTTP qui porte {@link apiKey}. Par défaut `X-API-Key`. */
167
+ readonly apiKeyHeader: string;
168
+
158
169
  constructor(params: {
159
170
  apiBaseUrl: string;
160
171
  accessToken: string;
161
172
  onAccessTokenExpired: () => Promise<string | null>;
162
173
  onSessionExpired: () => Promise<void>;
163
174
  demoMode?: boolean;
175
+ apiKey?: string;
176
+ apiKeyHeader?: string;
164
177
  }) {
165
178
  this.apiBaseUrl = params.apiBaseUrl;
166
179
  this.accessToken = params.accessToken;
167
180
  this.onAccessTokenExpired = params.onAccessTokenExpired;
168
181
  this.onSessionExpired = params.onSessionExpired;
169
182
  this.demoMode = params.demoMode ?? false;
183
+ this.apiKey = params.apiKey;
184
+ this.apiKeyHeader = params.apiKeyHeader ?? 'X-API-Key';
170
185
  }
171
186
  }
172
187
 
@@ -214,6 +229,18 @@ export class PiSpiAppConfig {
214
229
  */
215
230
  readonly defaultTheme?: Themes;
216
231
 
232
+ /**
233
+ * Thème personnalisé de l'application hôte, quand aucun des thèmes du
234
+ * SDK (`Themes`) ne convient : défini par ses couleurs ou par un
235
+ * `ThemeData` complet (voir {@link PiSpiCustomTheme}). Optionnel : si
236
+ * fourni, il est appliqué et **persisté** à chaque ouverture du SDK, et
237
+ * il prime sur {@link defaultTheme}. L'entrée « Thème » des paramètres
238
+ * est alors masquée, sauf si {@link canChangeTheme} vaut `true` : le
239
+ * client peut alors choisir entre « Thème de l'application » (ce thème)
240
+ * et les thèmes du SDK.
241
+ */
242
+ readonly customTheme?: PiSpiCustomTheme;
243
+
217
244
  /**
218
245
  * Autorise le client à changer lui-même de thème depuis les écrans de
219
246
  * paramètres du SDK ; sinon (`false`), l'entrée « Thème » y est
@@ -221,8 +248,8 @@ export class PiSpiAppConfig {
221
248
  * la préférence déjà persistée, voir sa documentation) pour toute la
222
249
  * session — comme pour {@link defaultLocale}.
223
250
  *
224
- * Non fourni, sa valeur par défaut dépend de {@link defaultTheme} :
225
- * `false` si l'hôte a fourni un `defaultTheme` (il impose alors ce
251
+ * Non fourni, sa valeur par défaut dépend de {@link defaultTheme} et de
252
+ * {@link customTheme} : `false` si l'hôte a fourni l'un des deux (il impose alors ce
226
253
  * thème, cohérent avec le comportement de `defaultLocale`), `true` sinon
227
254
  * (l'hôte n'a exprimé aucun avis sur le thème, donc autant laisser le
228
255
  * client choisir). Passez explicitement `true`/`false` pour imposer un
@@ -241,6 +268,7 @@ export class PiSpiAppConfig {
241
268
  appstoreLink?: string;
242
269
  defaultLocale?: SupportedLocaleCode;
243
270
  defaultTheme?: Themes;
271
+ customTheme?: PiSpiCustomTheme;
244
272
  canChangeTheme?: boolean;
245
273
  }) {
246
274
  this.pspCodeMembre = params.pspCodeMembre;
@@ -253,7 +281,8 @@ export class PiSpiAppConfig {
253
281
  this.appstoreLink = params.appstoreLink ?? '';
254
282
  this.defaultLocale = params.defaultLocale ?? 'fr';
255
283
  this.defaultTheme = params.defaultTheme;
256
- this.canChangeTheme = params.canChangeTheme ?? params.defaultTheme === undefined;
284
+ this.customTheme = params.customTheme;
285
+ this.canChangeTheme = params.canChangeTheme ?? (params.defaultTheme === undefined && params.customTheme === undefined);
257
286
 
258
287
  devAssert(
259
288
  (PiSpiAppConfig.supportedLocaleCodes as readonly string[]).includes(this.defaultLocale),
package/src/core/theme.ts CHANGED
@@ -384,6 +384,29 @@ export class Themer {
384
384
  switchActiveTrack: Themer.primary,
385
385
  };
386
386
 
387
+ /** Code enregistré comme thème préféré quand l'hôte fournit un thème personnalisé (`PiSpiAppConfig.customTheme`). */
388
+ static readonly customThemeCode = 'customTheme';
389
+
390
+ /** Thème personnalisé de l'hôte, construit à l'ouverture du SDK (voir `Di.init`) ; `null` sans thème personnalisé. */
391
+ static customTheme: ThemeData | null = null;
392
+
393
+ /**
394
+ * Thème clair construit à partir de couleurs (thème personnalisé de
395
+ * l'hôte, voir `PiSpiCustomTheme`) : même modèle que les thèmes rouge,
396
+ * violet ou rose, avec le texte des boutons en blanc ou en noir selon la
397
+ * luminosité de [primary].
398
+ */
399
+ static colored(primary: string, secondary: string, tertiary: string, pressed: string): ThemeData {
400
+ const onPrimary = relativeLuminance(primary) > 0.5 ? Themer.blackColor : Themer.whiteColor;
401
+ return Themer.build(
402
+ Themer.coloredSpec(primary, secondary, tertiary, {
403
+ schemeOnPrimary: onPrimary,
404
+ buttonPressed: pressed,
405
+ buttonForeground: onPrimary,
406
+ }),
407
+ );
408
+ }
409
+
387
410
  /** Variante claire monochrome (même structure pour jaune, bleu, vert, rouge, orange, violet). */
388
411
  private static coloredSpec(
389
412
  main: string,
@@ -486,6 +509,8 @@ export class Themer {
486
509
  * ponctuellement via `Appearance`.
487
510
  */
488
511
  static get(code: string | null | undefined, systemScheme?: ColorSchemeName | null): ThemeData {
512
+ // Thème personnalisé de l'hôte
513
+ if (code === Themer.customThemeCode && Themer.customTheme != null) return Themer.customTheme;
489
514
  switch (code) {
490
515
  case Themes.light:
491
516
  return Themer.lightTheme;
@@ -523,3 +548,31 @@ export const ThemeContext = createContext<ThemeData>(Themer.lightTheme);
523
548
  export function useTheme(): ThemeData {
524
549
  return useContext(ThemeContext);
525
550
  }
551
+
552
+ /** Composantes RGB (0-255) d'une couleur `#RRGGBB` */
553
+ export function hexToRgb(hex: string): [number, number, number] {
554
+ const value = hex.replace('#', '');
555
+ const full = value.length === 3 ? value.split('').map((c) => c + c).join('') : value.substring(0, 6);
556
+ return [0, 2, 4].map((i) => parseInt(full.substring(i, i + 2), 16)) as [number, number, number];
557
+ }
558
+
559
+ /** Couleur `#RRGGBB` à partir de ses composantes RGB */
560
+ export function rgbToHex([r, g, b]: [number, number, number]): string {
561
+ return `#${[r, g, b].map((c) => Math.round(Math.min(255, Math.max(0, c))).toString(16).padStart(2, '0')).join('').toUpperCase()}`;
562
+ }
563
+
564
+ /** Mélange [from] vers [to] à [t] (0 = from, 1 = to), comme `Color.lerp` en Flutter */
565
+ export function lerpColor(from: string, to: string, t: number): string {
566
+ const a = hexToRgb(from);
567
+ const b = hexToRgb(to);
568
+ return rgbToHex([0, 1, 2].map((i) => a[i] + (b[i] - a[i]) * t) as [number, number, number]);
569
+ }
570
+
571
+ /** Luminance relative (0 à 1), comme `Color.computeLuminance` en Flutter */
572
+ export function relativeLuminance(hex: string): number {
573
+ const [r, g, b] = hexToRgb(hex).map((c) => {
574
+ const s = c / 255;
575
+ return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
576
+ });
577
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b;
578
+ }
package/src/index.ts CHANGED
@@ -21,7 +21,8 @@ export {
21
21
  PiSpiLogConfig,
22
22
  type PiSpiLogLevel,
23
23
  } from './core/pi_spi_sdk_config';
24
- export { Themes } from './core/theme';
24
+ export { PiSpiCustomTheme } from './core/pi_spi_custom_theme';
25
+ export { Themes, type ThemeData } from './core/theme';
25
26
  export {
26
27
  type AppLocalizations,
27
28
  type SupportedLocaleCode,
@@ -487,6 +487,10 @@ export interface AppLocalizations {
487
487
  readonly homeActionMoreSheetAWidgetSubTitle: string;
488
488
  /** See All */
489
489
  readonly transactionsSeeAll: string;
490
+ /** Today */
491
+ readonly transactionsAujourdhui: string;
492
+ /** Yesterday */
493
+ readonly transactionsHier: string;
490
494
  /** Search for a contact */
491
495
  readonly transactionsSendInputHint: string;
492
496
  /** Use Alias */
@@ -1205,6 +1209,8 @@ export interface AppLocalizations {
1205
1209
  readonly appSettingPageMenuThemePurple: string;
1206
1210
  /** Pink */
1207
1211
  readonly appSettingPageMenuThemePink: string;
1212
+ /** App theme */
1213
+ readonly appSettingPageMenuThemeCustom: string;
1208
1214
  /** System */
1209
1215
  readonly appSettingPageMenuThemeDefault: string;
1210
1216
  /** Apparence */
@@ -244,6 +244,8 @@ Check your Wi-Fi or mobile data, then tap Retry.`,
244
244
  homeActionMoreSheetWidgetTitle: `Add Widget`,
245
245
  homeActionMoreSheetAWidgetSubTitle: `Manage widgets for home`,
246
246
  transactionsSeeAll: `See All`,
247
+ transactionsAujourdhui: `Today`,
248
+ transactionsHier: `Yesterday`,
247
249
  transactionsSendInputHint: `Search for a contact`,
248
250
  transactionsSendOptionAliasTitle: `Use Alias`,
249
251
  transactionsSendOptionAliasBSubtitle: `Payment address or phone no.`,
@@ -614,6 +616,7 @@ Scanning will be automatic`,
614
616
  appSettingPageMenuThemeOrange: `Orange`,
615
617
  appSettingPageMenuThemePurple: `Purple`,
616
618
  appSettingPageMenuThemePink: `Pink`,
619
+ appSettingPageMenuThemeCustom: `App theme`,
617
620
  appSettingPageMenuThemeDefault: `System`,
618
621
  appSettingPageMenuThemePageTitle: `Apparence`,
619
622
  appSettingPageMenuQrCodeTitle: `My QR code by default`,
@@ -244,6 +244,8 @@ Vérifiez votre Wi-Fi ou vos données mobiles puis appuyez sur Réessayer.`,
244
244
  homeActionMoreSheetWidgetTitle: `Ajouter un widget`,
245
245
  homeActionMoreSheetAWidgetSubTitle: `Gérer les widgets de l’écran principal`,
246
246
  transactionsSeeAll: `Tout afficher`,
247
+ transactionsAujourdhui: `Aujourd’hui`,
248
+ transactionsHier: `Hier`,
247
249
  transactionsSendInputHint: `Rechercher un contact`,
248
250
  transactionsSendOptionAliasTitle: `Par alias`,
249
251
  transactionsSendOptionAliasBSubtitle: `Adresse de paiement ou n° de téléphone`,
@@ -613,6 +615,7 @@ Le scan se fait automatiquement`,
613
615
  appSettingPageMenuThemeOrange: `Orange`,
614
616
  appSettingPageMenuThemePurple: `Violet`,
615
617
  appSettingPageMenuThemePink: `Rose`,
618
+ appSettingPageMenuThemeCustom: `Thème de l'application`,
616
619
  appSettingPageMenuThemeDefault: `Système`,
617
620
  appSettingPageMenuThemePageTitle: `Apparence`,
618
621
  appSettingPageMenuQrCodeTitle: `Mon QR code par défaut`,
@@ -244,6 +244,8 @@ Verifique o Wi-Fi ou os dados móveis e toque em Tentar novamente.`,
244
244
  homeActionMoreSheetWidgetTitle: `Adicionar um widget`,
245
245
  homeActionMoreSheetAWidgetSubTitle: `Gerenciar os widgets do ecrã principal`,
246
246
  transactionsSeeAll: `Todas as transações`,
247
+ transactionsAujourdhui: `Hoje`,
248
+ transactionsHier: `Ontem`,
247
249
  transactionsSendInputHint: `Pesquisar um contato`,
248
250
  transactionsSendOptionAliasTitle: `Por alias`,
249
251
  transactionsSendOptionAliasBSubtitle: `Endereço de pagamento ou n° de telefone`,
@@ -614,6 +616,7 @@ O scan é feito automaticamente`,
614
616
  appSettingPageMenuThemeOrange: `Laranja`,
615
617
  appSettingPageMenuThemePurple: `Roxo`,
616
618
  appSettingPageMenuThemePink: `Rosa`,
619
+ appSettingPageMenuThemeCustom: `Tema da aplicação`,
617
620
  appSettingPageMenuThemeDefault: `Sistema`,
618
621
  appSettingPageMenuThemePageTitle: `Aparência`,
619
622
  appSettingPageMenuQrCodeTitle: `Meu QR Code por defeito`,
@@ -90,7 +90,7 @@ export function HomeTabCompte() {
90
90
 
91
91
  const styles = StyleSheet.create({
92
92
  content: { paddingHorizontal: 16, paddingVertical: 12 },
93
- actions: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center', paddingHorizontal: 16 },
93
+ actions: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' },
94
94
  gap10: { height: 10 },
95
95
  gap14: { height: 14 },
96
96
  gap16: { height: 16 },
@@ -1,4 +1,5 @@
1
1
  import type { VideoPlayer } from 'expo-video';
2
+ import { useState } from 'react';
2
3
  import { StyleSheet, View } from 'react-native';
3
4
 
4
5
  import type { IntroductionItem } from './introduction_item';
@@ -15,6 +16,11 @@ export function IntroductionBar({
15
16
  pageSize: number;
16
17
  progressBarWidth: number;
17
18
  }) {
19
+ // Largeur des segments tirée de la place réellement disponible, pas de
20
+ // [progressBarWidth] (calculé sur tout l'écran) : sur un écran déplié
21
+ // (Galaxy Z Fold), les marges système la réduisent et la barre débordait
22
+ const [largeur, setLargeur] = useState<number | null>(null);
23
+ const itemWidth = largeur != null ? largeur / pageSize - 4 : progressBarWidth;
18
24
  const widgetPosition = item.position;
19
25
  const barList = [];
20
26
  for (let i = 0; i < pageSize; i++) {
@@ -23,13 +29,17 @@ export function IntroductionBar({
23
29
  <VideoProgressBar
24
30
  key={i}
25
31
  videoPlayer={i === widgetPosition ? videoPlayer : null}
26
- progressBarItemWidth={progressBarWidth}
32
+ progressBarItemWidth={itemWidth}
27
33
  passed={i < widgetPosition}
28
34
  current={i === widgetPosition}
29
35
  />,
30
36
  );
31
37
  }
32
- return <View style={styles.row}>{barList}</View>;
38
+ return (
39
+ <View style={styles.row} onLayout={(event) => setLargeur(event.nativeEvent.layout.width)}>
40
+ {barList}
41
+ </View>
42
+ );
33
43
  }
34
44
 
35
45
  const styles = StyleSheet.create({
@@ -1,5 +1,5 @@
1
1
  import { useVideoPlayer } from 'expo-video';
2
- import { useEffect } from 'react';
2
+ import { useEffect, useState } from 'react';
3
3
  import { Pressable, StyleSheet, View } from 'react-native';
4
4
 
5
5
  import { useAppLocalizations } from '../../../../l10n/localizations';
@@ -28,6 +28,11 @@ export function IntroductionPageItem({
28
28
  onNext: () => void;
29
29
  }) {
30
30
  const traductions = useAppLocalizations();
31
+ // Vidéo et titres réduits si la hauteur manque (écran déplié, paysage),
32
+ // jamais agrandis : hauteur disponible / hauteur naturelle du bloc
33
+ const [hauteurZone, setHauteurZone] = useState(0);
34
+ const [hauteurContenu, setHauteurContenu] = useState(0);
35
+ const echelle = hauteurZone > 0 && hauteurContenu > hauteurZone ? hauteurZone / hauteurContenu : 1;
31
36
  const videoPlayer = useVideoPlayer(item.video, (player) => {
32
37
  player.loop = true;
33
38
  player.volume = 1.0;
@@ -59,7 +64,19 @@ export function IntroductionPageItem({
59
64
  {/* barre de progression */}
60
65
  <IntroductionBar videoPlayer={videoPlayer} item={item} pageSize={pageSize} progressBarWidth={progressBarWidth} />
61
66
  {/* Video et titres */}
62
- <IntroductionVideo videoPlayer={videoPlayer} item={item} />
67
+ <View
68
+ style={styles.zoneVideo}
69
+ pointerEvents="box-none"
70
+ onLayout={(event) => setHauteurZone(event.nativeEvent.layout.height)}
71
+ >
72
+ <View
73
+ style={{ transform: [{ scale: echelle }] }}
74
+ pointerEvents="box-none"
75
+ onLayout={(event) => setHauteurContenu(event.nativeEvent.layout.height)}
76
+ >
77
+ <IntroductionVideo videoPlayer={videoPlayer} item={item} />
78
+ </View>
79
+ </View>
63
80
  {/* Pied de la page */}
64
81
  <IntroductionFooter item={item} pageSize={pageSize} traductions={traductions} />
65
82
  </View>
@@ -72,4 +89,5 @@ const styles = StyleSheet.create({
72
89
  side: { flex: 1 },
73
90
  middle: { flex: 2 },
74
91
  page: { flex: 1, paddingVertical: 12, paddingHorizontal: 20, alignItems: 'stretch', justifyContent: 'space-between' },
92
+ zoneVideo: { flexShrink: 1, marginVertical: 12, alignItems: 'center', justifyContent: 'center' },
75
93
  });
@@ -5,7 +5,7 @@ import { useBloc, useBlocBuilder } from '../../../../../core/bloc_react';
5
5
  import { Di } from '../../../../../core/di';
6
6
  import { AppRouter, useAppRouter } from '../../../../../core/router';
7
7
  import { SoundManager } from '../../../../../core/sounds';
8
- import { Themes, useTheme } from '../../../../../core/theme';
8
+ import { Themer, Themes, useTheme } from '../../../../../core/theme';
9
9
  import type { AppLocalizations } from '../../../../../l10n/app_localizations';
10
10
  import { useAppLocalizations } from '../../../../../l10n/localizations';
11
11
  import { AppBar, Scaffold } from '../../../../../shared/material/scaffold';
@@ -45,7 +45,10 @@ function menuItems(traductions: AppLocalizations, configParams: ConfigParams): P
45
45
  if (Di.config.appConfig.canChangeTheme) {
46
46
  items.push(
47
47
  new ProfileItem(Images.iconsTheme, traductions.appSettingPageMenuThemeTitle, AppRouter.profileParametreTheme, {
48
- subtitle: Themes.label(theme, traductions),
48
+ subtitle:
49
+ params[ConfigKey.preferedTheme] === Themer.customThemeCode
50
+ ? traductions.appSettingPageMenuThemeCustom
51
+ : Themes.label(theme, traductions),
49
52
  }),
50
53
  );
51
54
  }
@@ -1,10 +1,11 @@
1
1
  import { Image } from 'expo-image';
2
+ import type { ReactNode } from 'react';
2
3
  import { Pressable, ScrollView, StyleSheet, Text, View } from 'react-native';
3
4
 
4
5
  import { Images } from '../../../../../core/assets';
5
6
  import { useBloc, useBlocBuilder } from '../../../../../core/bloc_react';
6
7
  import { AppRouter, useAppRouter } from '../../../../../core/router';
7
- import { Themes, useTheme } from '../../../../../core/theme';
8
+ import { Themer, Themes, useTheme, type ThemeData } from '../../../../../core/theme';
8
9
  import type { AppLocalizations } from '../../../../../l10n/app_localizations';
9
10
  import { useAppLocalizations } from '../../../../../l10n/localizations';
10
11
  import { Card } from '../../../../../shared/material/card';
@@ -67,21 +68,23 @@ export function ProfileParametrePageTheme() {
67
68
  <View style={styles.gap10} />
68
69
  <Card>
69
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
+ />
79
+ )}
70
80
  {Themes.list().map((item) => (
71
- <Pressable
81
+ <ThemeOption
72
82
  key={item}
73
- accessibilityRole="button"
74
- disabled={currentTheme === item}
75
- onPress={() => configBloc.add(new ConfigChangeEvent(ConfigKey.preferedTheme, item))}
76
- style={styles.item}
77
- >
78
- <Image source={themeIcon(item)} style={[styles.preview, { backgroundColor: theme.colorScheme.surface }]} contentFit="cover" />
79
- <View style={styles.gap5} />
80
- <Text style={theme.textTheme.displayMedium}>{themeTitle(traductions, item)}</Text>
81
- <View style={styles.gap5} />
82
- <CheckedDot value={currentTheme === item} />
83
- <View style={styles.gap20} />
84
- </Pressable>
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
+ />
85
88
  ))}
86
89
  </View>
87
90
  </Card>
@@ -91,10 +94,87 @@ export function ProfileParametrePageTheme() {
91
94
  );
92
95
  }
93
96
 
97
+ /** Une option de thème : aperçu, nom et pastille de sélection */
98
+ function ThemeOption({
99
+ title,
100
+ selected,
101
+ onSelect,
102
+ preview,
103
+ }: {
104
+ title: string;
105
+ selected: boolean;
106
+ onSelect: () => void;
107
+ preview: ReactNode;
108
+ }) {
109
+ const theme = useTheme();
110
+ 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} />
118
+ </Pressable>
119
+ );
120
+ }
121
+
122
+ /**
123
+ * Aperçu du thème personnalisé de l'hôte (pas d'image comme pour les
124
+ * thèmes du SDK) : une miniature d'accueil dessinée avec ses couleurs.
125
+ */
126
+ function CustomThemePreview({ theme }: { theme: ThemeData }) {
127
+ const primary = theme.colorScheme.primary;
128
+ const secondary = theme.colorScheme.secondary;
129
+ const bar = (width: number, color: string) => <View style={[styles.bar, { width, backgroundColor: color }]} />;
130
+ return (
131
+ <View style={[styles.flex, styles.mock, { backgroundColor: theme.scaffoldBackgroundColor }]}>
132
+ {/* Avatar et titre */}
133
+ <View style={styles.mockRow}>
134
+ <View style={[styles.dot16, { backgroundColor: secondary }]} />
135
+ {bar(40, Themer.neural02Color)}
136
+ </View>
137
+ <View style={styles.gap12} />
138
+ {/* Carte du compte */}
139
+ <View style={[styles.mockCard, { backgroundColor: theme.cardColor }]}>
140
+ {bar(50, Themer.neural02Color)}
141
+ {bar(30, primary)}
142
+ </View>
143
+ <View style={styles.gap12} />
144
+ {/* Boutons d'action */}
145
+ <View style={styles.mockActions}>
146
+ {[0, 1, 2].map((i) => (
147
+ <View key={i} style={[styles.mockButton, { backgroundColor: primary }]} />
148
+ ))}
149
+ </View>
150
+ <View style={styles.gap14} />
151
+ {/* Liste */}
152
+ {[0, 1, 2].map((i) => (
153
+ <View key={i} style={[styles.mockRow, styles.mockListRow]}>
154
+ <View style={[styles.dot14, { backgroundColor: secondary }]} />
155
+ {bar(46, Themer.neural02Color)}
156
+ </View>
157
+ ))}
158
+ </View>
159
+ );
160
+ }
161
+
94
162
  const styles = StyleSheet.create({
163
+ flex: { flex: 1 },
164
+ mock: { padding: 10 },
165
+ mockRow: { flexDirection: 'row', alignItems: 'center', gap: 6 },
166
+ mockListRow: { marginBottom: 8 },
167
+ mockCard: { height: 54, padding: 8, borderRadius: 8, justifyContent: 'space-between' },
168
+ mockActions: { flexDirection: 'row', justifyContent: 'space-between' },
169
+ mockButton: { width: 26, height: 26, borderRadius: 8 },
170
+ bar: { height: 6, borderRadius: 3 },
171
+ dot16: { width: 16, height: 16, borderRadius: 8 },
172
+ dot14: { width: 14, height: 14, borderRadius: 7 },
173
+ gap12: { height: 12 },
174
+ gap14: { height: 14 },
95
175
  wrap: { padding: 20, flexDirection: 'row', flexWrap: 'wrap', justifyContent: 'space-between', columnGap: 10, rowGap: 10 },
96
176
  item: { alignItems: 'center' },
97
- preview: { width: 120, height: 200, borderRadius: 8 },
177
+ preview: { width: 120, height: 200, borderRadius: 8, overflow: 'hidden' },
98
178
  gap5: { height: 5 },
99
179
  gap10: { height: 10 },
100
180
  gap20: { height: 20 },
@@ -4,7 +4,7 @@ import { TransactionActions } from './transaction_actions';
4
4
  import { TransactionSendCommandAlias } from './transaction_send_command_alias';
5
5
  import { TransactionSendCommandAmount } from './transaction_send_command_amount';
6
6
  import type { TransactionSendCommandContact } from './transaction_send_command_contact';
7
- import type { TransactionSendCommandMotif } from './transaction_send_command_motif';
7
+ import { TransactionSendCommandMotif } from './transaction_send_command_motif';
8
8
  import { TransactionSendCommandOthr } from './transaction_send_command_othr';
9
9
  import type { TransactionSendCommandSchedule } from './transaction_send_command_schedule';
10
10
  import { TransactionSendMethod } from './transaction_send_method';
@@ -109,7 +109,7 @@ export class TransactionSendCommand {
109
109
  longitude: this.longitude ?? null,
110
110
  };
111
111
  if (this.txId != null) json.txId = this.txId;
112
- if (this.motif != null) json.motif = this.motif.value ?? null;
112
+ if (this.motif?.value != null && this.motif.value.length > 0) json.motif = this.motif.value;
113
113
  if (this.schedule != null) {
114
114
  json.dateDebut = this.schedule.dateDebut;
115
115
  if (this.schedule.frequence != null) {
@@ -163,6 +163,10 @@ export class TransactionSendCommand {
163
163
  pspPays: transaction.clientPays,
164
164
  pspNom: transaction.clientPSPNom,
165
165
  amount: new TransactionSendCommandAmount({ action, value: transaction.montant }),
166
+ motif:
167
+ transaction.motif != null && !transaction.motif.startsWith('@')
168
+ ? new TransactionSendCommandMotif({ value: transaction.motif })
169
+ : undefined,
166
170
  });
167
171
  }
168
172
  }
@@ -12,14 +12,17 @@ import { TransactionSendFormChangedEvent } from '../../bloc/transaction_send/tra
12
12
  export function TransactionFormInputMotif({
13
13
  command,
14
14
  transactionFormBloc,
15
+ readOnly = false,
15
16
  }: {
16
17
  command: TransactionSendCommand;
17
18
  transactionFormBloc: TransactionSendBloc;
19
+ readOnly?: boolean;
18
20
  }) {
19
21
  const traductions = useAppLocalizations();
20
22
  return (
21
23
  <CustomTextMultipleInput
22
24
  labelText={traductions.transactionFormMotifHint}
25
+ readOnly={readOnly}
23
26
  defaultValue={command.motif?.value}
24
27
  messageError={command.motif?.error === TransactionSendCommandMotifError.invalid ? traductions.transactionFormMotifInvalid : ''}
25
28
  onChange={(value) => {
@@ -0,0 +1,17 @@
1
+ import type { AppLocalizations } from '../../../../l10n/app_localizations';
2
+ import { DateFormat } from '../../../../shared/utils/date_format';
3
+
4
+ /** « Aujourd'hui », « Hier » ou la date du jour [jour] (aaaa-mm-jj) */
5
+ export function transactionJourLibelle(traductions: AppLocalizations, jour: string): string {
6
+ const date = new Date(`${jour}T00:00:00`);
7
+ const maintenant = new Date();
8
+ // En UTC : pas de journée de 23 h aux changements d'heure
9
+ const ecart = Math.round(
10
+ (Date.UTC(maintenant.getFullYear(), maintenant.getMonth(), maintenant.getDate()) -
11
+ Date.UTC(date.getFullYear(), date.getMonth(), date.getDate())) /
12
+ 86400000,
13
+ );
14
+ if (ecart === 0) return traductions.transactionsAujourdhui;
15
+ if (ecart === 1) return traductions.transactionsHier;
16
+ return new DateFormat('d MMMM y', traductions.localeName).format(date);
17
+ }
@@ -16,12 +16,14 @@ import { TransactionSens, type Transaction } from '../../domain/models/transacti
16
16
  */
17
17
  export function TransactionListItemWidget({
18
18
  transaction,
19
+ datePartern = 'd MMM, HH:mm',
19
20
  detailsBackRoute,
20
21
  noLink,
21
22
  onSelect,
22
23
  isSelected,
23
24
  }: {
24
25
  transaction: Transaction;
26
+ datePartern?: string;
25
27
  detailsBackRoute?: string;
26
28
  noLink?: boolean;
27
29
  onSelect?: (value: boolean) => void;
@@ -65,7 +67,7 @@ export function TransactionListItemWidget({
65
67
  {/* Date d'irrévocabilité */}
66
68
  {transaction.dateOperation != null && (
67
69
  <Text style={[theme.textTheme.displaySmall, { color: Themer.neural04Color }]}>
68
- {new DateFormat('d MMM, HH:mm', locale).format(transaction.dateOperation)}
70
+ {new DateFormat(datePartern, locale).format(transaction.dateOperation)}
69
71
  </Text>
70
72
  )}
71
73
  </View>