From e1516366dc79365b72c8fcd411a58e6f5db3dfe6 Mon Sep 17 00:00:00 2001 From: Guilherme Afonso Date: Tue, 18 Feb 2025 23:37:19 -0300 Subject: [PATCH 01/25] feat: add top up page structure --- src/app/app.routes.ts | 5 ++ .../card-button/card-button.component.html | 14 +++++ .../card-button/card-button.component.ts | 15 +++++ .../card-group/card-group.component.html | 2 + .../cards/card-group/card-group.component.ts | 12 ++++ .../pages/top-up/top-up.component copy.html | 48 ++++++++++++++++ src/app/pages/top-up/top-up.component.html | 42 ++++++++++++++ src/app/pages/top-up/top-up.component.ts | 55 +++++++++++++++++++ .../general-goods/general-goods.service.ts | 1 + 9 files changed, 194 insertions(+) create mode 100644 src/app/components/cards/card-button/card-button.component.html create mode 100644 src/app/components/cards/card-button/card-button.component.ts create mode 100644 src/app/components/cards/card-group/card-group.component.html create mode 100644 src/app/components/cards/card-group/card-group.component.ts create mode 100644 src/app/pages/top-up/top-up.component copy.html create mode 100644 src/app/pages/top-up/top-up.component.html create mode 100644 src/app/pages/top-up/top-up.component.ts diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index 1cfd3e2..73ec125 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -84,6 +84,11 @@ export const routes: Routes = [ (m) => m.LegalTermsPageComponent, ), }, + { + path: 'top-up', + loadComponent: () => + import('./pages/top-up/top-up.component').then((m) => m.TopUpComponent), + }, { path: '404', loadComponent: () => diff --git a/src/app/components/cards/card-button/card-button.component.html b/src/app/components/cards/card-button/card-button.component.html new file mode 100644 index 0000000..98b7ef1 --- /dev/null +++ b/src/app/components/cards/card-button/card-button.component.html @@ -0,0 +1,14 @@ +
+ +
+

+

+ {{ subtitle() }} +

+
+ +
diff --git a/src/app/components/cards/card-button/card-button.component.ts b/src/app/components/cards/card-button/card-button.component.ts new file mode 100644 index 0000000..58f558a --- /dev/null +++ b/src/app/components/cards/card-button/card-button.component.ts @@ -0,0 +1,15 @@ +import { ChangeDetectionStrategy, Component, input } from '@angular/core'; + +import { NgIcon } from '@ng-icons/core'; + +@Component({ + // eslint-disable-next-line @angular-eslint/component-selector + selector: 'button[rusbe-card-button]', + imports: [NgIcon], + templateUrl: './card-button.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + host: { class: 'grouping-card-strip-button w-full' }, +}) +export class CardButtonComponent { + subtitle = input(''); +} diff --git a/src/app/components/cards/card-group/card-group.component.html b/src/app/components/cards/card-group/card-group.component.html new file mode 100644 index 0000000..e165999 --- /dev/null +++ b/src/app/components/cards/card-group/card-group.component.html @@ -0,0 +1,2 @@ +

{{ title() }}

+ diff --git a/src/app/components/cards/card-group/card-group.component.ts b/src/app/components/cards/card-group/card-group.component.ts new file mode 100644 index 0000000..dcdc5d6 --- /dev/null +++ b/src/app/components/cards/card-group/card-group.component.ts @@ -0,0 +1,12 @@ +import { ChangeDetectionStrategy, Component, input } from '@angular/core'; + +@Component({ + // eslint-disable-next-line @angular-eslint/component-selector + selector: 'section[rusbe-card-group]', + templateUrl: './card-group.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + host: { class: 'grouping-card' }, +}) +export class CardGroupComponent { + title = input(''); +} diff --git a/src/app/pages/top-up/top-up.component copy.html b/src/app/pages/top-up/top-up.component copy.html new file mode 100644 index 0000000..402cb5a --- /dev/null +++ b/src/app/pages/top-up/top-up.component copy.html @@ -0,0 +1,48 @@ +
+
+ ADICIONAR CRÉDITOS +
+ +
+
+ @if (true) { +
+

PELO APP

+ @for (transactionType of appTransactionTypes(); track $index) { + + } +
+ +
+

PRESENCIALMENTE NO GUICHÊ

+
+ } @else { +
+ +

Um momento...

+
+ } +
+
+
diff --git a/src/app/pages/top-up/top-up.component.html b/src/app/pages/top-up/top-up.component.html new file mode 100644 index 0000000..ddae4e4 --- /dev/null +++ b/src/app/pages/top-up/top-up.component.html @@ -0,0 +1,42 @@ +
+
+ ADICIONAR CRÉDITOS +
+ +
+
+ @if (true) { +
+ @for (transactionType of appTransactionTypes; track $index) { + + } +
+ +
+ @for (transactionType of inLocoTransactionTypes; track $index) { + + } +
+ } @else { +
+ +

Um momento...

+
+ } +
+
+
diff --git a/src/app/pages/top-up/top-up.component.ts b/src/app/pages/top-up/top-up.component.ts new file mode 100644 index 0000000..2f30fa6 --- /dev/null +++ b/src/app/pages/top-up/top-up.component.ts @@ -0,0 +1,55 @@ +import { ChangeDetectionStrategy, Component, signal } from '@angular/core'; + +import { NgIcon, provideIcons } from '@ng-icons/core'; +import { + lucideChevronRight, + lucideEraser, + lucideFullscreen, + lucideListStart, + lucidePalette, + lucideRotateCcw, + lucideUtensils, +} from '@ng-icons/lucide'; + +import { CardButtonComponent } from '@rusbe/components/cards/card-button/card-button.component'; +import { CardGroupComponent } from '@rusbe/components/cards/card-group/card-group.component'; +import { + HeaderComponent, + HeaderType, +} from '@rusbe/components/header/header.component'; + +import { GeneralGoodsTransactionType } from './../../services/general-goods/general-goods.service'; + +@Component({ + selector: 'rusbe-top-up', + imports: [HeaderComponent, CardGroupComponent, CardButtonComponent, NgIcon], + templateUrl: './top-up.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + viewProviders: [ + provideIcons({ + lucidePalette, + lucideUtensils, + lucideListStart, + lucideFullscreen, + lucideRotateCcw, + lucideEraser, + lucideChevronRight, + }), + ], +}) +export class TopUpComponent { + readonly HEADER_TYPE = HeaderType.PageNameWithBackButton; + appTransactionTypes = Object.values(GeneralGoodsTransactionType); + inLocoTransactionTypes = [ + 'Em espécie', + 'Cartão de crédito', + 'Cartão de débito', + 'Pix', + ]; + TRANSACTION_TYPE_MESSAGES = { + [GeneralGoodsTransactionType.Pix]: + 'Use o código gerado para pagar usando Pix.', + [GeneralGoodsTransactionType.CreditCard]: + 'Use seu cartão de crédito para completar a transação.', + }; +} diff --git a/src/app/services/general-goods/general-goods.service.ts b/src/app/services/general-goods/general-goods.service.ts index e50c27c..034144e 100644 --- a/src/app/services/general-goods/general-goods.service.ts +++ b/src/app/services/general-goods/general-goods.service.ts @@ -545,4 +545,5 @@ export enum GeneralGoodsBalanceType { export enum GeneralGoodsTransactionType { Pix = 'Pix', + CreditCard = 'Cartão de crédito', } From 1c893c1005b95b7e1678f1784ef228bf5d1802db Mon Sep 17 00:00:00 2001 From: Guilherme Afonso Date: Wed, 19 Feb 2025 22:21:45 -0300 Subject: [PATCH 02/25] fix: remove template copy --- .../pages/top-up/top-up.component copy.html | 48 ------------------- 1 file changed, 48 deletions(-) delete mode 100644 src/app/pages/top-up/top-up.component copy.html diff --git a/src/app/pages/top-up/top-up.component copy.html b/src/app/pages/top-up/top-up.component copy.html deleted file mode 100644 index 402cb5a..0000000 --- a/src/app/pages/top-up/top-up.component copy.html +++ /dev/null @@ -1,48 +0,0 @@ -
-
- ADICIONAR CRÉDITOS -
- -
-
- @if (true) { -
-

PELO APP

- @for (transactionType of appTransactionTypes(); track $index) { - - } -
- -
-

PRESENCIALMENTE NO GUICHÊ

-
- } @else { -
- -

Um momento...

-
- } -
-
-
From 345b874c4951f460718c2f6e3fcc4ecbad3f8275 Mon Sep 17 00:00:00 2001 From: Guilherme Afonso Date: Sun, 23 Feb 2025 06:27:55 -0300 Subject: [PATCH 03/25] feat: basic flow --- .vscode/settings.json | 6 + package-lock.json | 15 ++ package.json | 1 + public/assets/icons/bootstrapCash.svg | 4 + public/assets/icons/bootstrapCreditCard.svg | 4 + public/assets/icons/bootstrapDebitCard.svg | 4 + public/assets/icons/pix.svg | 1 + src/app/app.config.ts | 8 +- src/app/app.routes.ts | 1 + .../card-button/card-button.component.html | 36 ++-- .../card-button/card-button.component.ts | 3 +- .../card-group/card-group.component.html | 4 +- .../back-button/back-button.component.ts | 4 + .../components/header/header.component.html | 1 + src/app/components/header/header.component.ts | 1 + .../top-up-calculator.component.html | 49 ++++++ .../calculator/top-up-calculator.component.ts | 87 ++++++++++ .../top-up-credit-card.component.html | 2 + .../top-up-credit-card.component.ts | 9 + .../top-up-in-loco-helper.component.html | 46 ++++++ .../top-up-in-loco-helper.component.ts | 49 ++++++ .../payment-method.component.html | 37 +++++ .../payment-method.component.ts | 80 +++++++++ .../top-up/pix/top-up-pix.component.html | 104 ++++++++++++ .../top-up/pix/top-up-pix.component.ts | 52 ++++++ src/app/pages/home/home.component.html | 9 +- src/app/pages/top-up/top-up.component.html | 93 ++++++----- src/app/pages/top-up/top-up.component.ts | 155 +++++++++++++----- src/styles.css | 4 + 29 files changed, 763 insertions(+), 106 deletions(-) create mode 100644 public/assets/icons/bootstrapCash.svg create mode 100644 public/assets/icons/bootstrapCreditCard.svg create mode 100644 public/assets/icons/bootstrapDebitCard.svg create mode 100644 public/assets/icons/pix.svg create mode 100644 src/app/components/top-up/calculator/top-up-calculator.component.html create mode 100644 src/app/components/top-up/calculator/top-up-calculator.component.ts create mode 100644 src/app/components/top-up/credit-card/top-up-credit-card.component.html create mode 100644 src/app/components/top-up/credit-card/top-up-credit-card.component.ts create mode 100644 src/app/components/top-up/in-loco-helper/top-up-in-loco-helper.component.html create mode 100644 src/app/components/top-up/in-loco-helper/top-up-in-loco-helper.component.ts create mode 100644 src/app/components/top-up/payment-method/payment-method.component.html create mode 100644 src/app/components/top-up/payment-method/payment-method.component.ts create mode 100644 src/app/components/top-up/pix/top-up-pix.component.html create mode 100644 src/app/components/top-up/pix/top-up-pix.component.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 1947a0d..0eb3265 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,5 +2,11 @@ "editor.formatOnSave": true, "editor.codeActionsOnSave": { "source.fixAll.eslint": "explicit" + }, + "angular-schematics.schematicsDefaultOptions": { + "angular-*": { + "skipStyle": true, + "externalTemplate": true + } } } diff --git a/package-lock.json b/package-lock.json index 3e8f0f9..748fdcd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,6 +27,7 @@ "date-fns": "^4.1.0", "idb-keyval": "^6.2.1", "jwt-decode": "^4.0.0", + "ngx-currency": "^19.0.0", "ngx-mask": "^19.0.6", "rxjs": "~7.8.2", "superjson": "^2.2.2", @@ -14801,6 +14802,20 @@ "dev": true, "license": "MIT" }, + "node_modules/ngx-currency": { + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/ngx-currency/-/ngx-currency-19.0.0.tgz", + "integrity": "sha512-N7SdB8D+ttPAf9GNiMyhChPUJs0t0mRTMiJ2UILWN83Vnx9dGc1ytjFrV7q3pCVA66svl1bpSFOfbZ2rhJ7zHA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.5.0" + }, + "peerDependencies": { + "@angular/common": "^19.0.0", + "@angular/core": "^19.0.0", + "@angular/forms": "^19.0.0" + } + }, "node_modules/ngx-mask": { "version": "19.0.6", "resolved": "https://registry.npmjs.org/ngx-mask/-/ngx-mask-19.0.6.tgz", diff --git a/package.json b/package.json index 0dc8424..c254335 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "date-fns": "^4.1.0", "idb-keyval": "^6.2.1", "jwt-decode": "^4.0.0", + "ngx-currency": "^19.0.0", "ngx-mask": "^19.0.6", "rxjs": "~7.8.2", "superjson": "^2.2.2", diff --git a/public/assets/icons/bootstrapCash.svg b/public/assets/icons/bootstrapCash.svg new file mode 100644 index 0000000..18cbff3 --- /dev/null +++ b/public/assets/icons/bootstrapCash.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/public/assets/icons/bootstrapCreditCard.svg b/public/assets/icons/bootstrapCreditCard.svg new file mode 100644 index 0000000..406233d --- /dev/null +++ b/public/assets/icons/bootstrapCreditCard.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/public/assets/icons/bootstrapDebitCard.svg b/public/assets/icons/bootstrapDebitCard.svg new file mode 100644 index 0000000..b29419c --- /dev/null +++ b/public/assets/icons/bootstrapDebitCard.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/public/assets/icons/pix.svg b/public/assets/icons/pix.svg new file mode 100644 index 0000000..4304e41 --- /dev/null +++ b/public/assets/icons/pix.svg @@ -0,0 +1 @@ + diff --git a/src/app/app.config.ts b/src/app/app.config.ts index a01683b..ccc0811 100644 --- a/src/app/app.config.ts +++ b/src/app/app.config.ts @@ -1,4 +1,4 @@ -import { provideHttpClient } from '@angular/common/http'; +import { HttpClient, provideHttpClient } from '@angular/common/http'; import { ApplicationConfig, inject, @@ -25,6 +25,8 @@ import { initializeApp, provideFirebaseApp } from '@angular/fire/app'; import { getAuth, provideAuth } from '@angular/fire/auth'; import { getFirestore, provideFirestore } from '@angular/fire/firestore'; +import { provideNgIconLoader } from '@ng-icons/core'; + import { routes } from '@rusbe/app.routes'; import { environment } from '@rusbe/environments/environment'; import { version } from '@rusbe/environments/version'; @@ -69,5 +71,9 @@ export const appConfig: ApplicationConfig = { }), ScreenTrackingService, UserTrackingService, + provideNgIconLoader((name) => { + const http = inject(HttpClient); + return http.get(`/assets/icons/${name}.svg`, { responseType: 'text' }); + }), ], }; diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index 73ec125..e848e42 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -88,6 +88,7 @@ export const routes: Routes = [ path: 'top-up', loadComponent: () => import('./pages/top-up/top-up.component').then((m) => m.TopUpComponent), + ...canActivate(redirectUnauthorizedToLogin), }, { path: '404', diff --git a/src/app/components/cards/card-button/card-button.component.html b/src/app/components/cards/card-button/card-button.component.html index 98b7ef1..c245e7d 100644 --- a/src/app/components/cards/card-button/card-button.component.html +++ b/src/app/components/cards/card-button/card-button.component.html @@ -1,14 +1,22 @@ -
- -
-

-

- {{ subtitle() }} -

-
- -
+
+ @if (iconName()) { + + } @else { +
+ } +
+

+

+ {{ subtitle() }} +

+
+ +
diff --git a/src/app/components/cards/card-button/card-button.component.ts b/src/app/components/cards/card-button/card-button.component.ts index 58f558a..57e8b62 100644 --- a/src/app/components/cards/card-button/card-button.component.ts +++ b/src/app/components/cards/card-button/card-button.component.ts @@ -8,8 +8,9 @@ import { NgIcon } from '@ng-icons/core'; imports: [NgIcon], templateUrl: './card-button.component.html', changeDetection: ChangeDetectionStrategy.OnPush, - host: { class: 'grouping-card-strip-button w-full' }, + host: { class: 'grouping-card-strip-button w-full disabled:opacity-50' }, }) export class CardButtonComponent { subtitle = input(''); + iconName = input(''); } diff --git a/src/app/components/cards/card-group/card-group.component.html b/src/app/components/cards/card-group/card-group.component.html index e165999..5afe185 100644 --- a/src/app/components/cards/card-group/card-group.component.html +++ b/src/app/components/cards/card-group/card-group.component.html @@ -1,2 +1,2 @@ -

{{ title() }}

- +

{{ title() }}

+ diff --git a/src/app/components/header/back-button/back-button.component.ts b/src/app/components/header/back-button/back-button.component.ts index 9bdbcfc..3428331 100644 --- a/src/app/components/header/back-button/back-button.component.ts +++ b/src/app/components/header/back-button/back-button.component.ts @@ -15,12 +15,16 @@ import { ButtonColorScheme } from '@rusbe/components/header/button-color-scheme' }) export class BackButtonComponent { colorScheme = input(ButtonColorScheme.Page); + customAction = input<(() => boolean) | null>(null); ButtonColorScheme = ButtonColorScheme; router = inject(Router); location = inject(Location); goBack() { + const customAction = this.customAction()?.(); + if (customAction) return; + if (this.router.lastSuccessfulNavigation?.previousNavigation != null) { this.location.back(); } else { diff --git a/src/app/components/header/header.component.html b/src/app/components/header/header.component.html index adc654a..4046a77 100644 --- a/src/app/components/header/header.component.html +++ b/src/app/components/header/header.component.html @@ -9,6 +9,7 @@

@if (hasBackButton()) { } diff --git a/src/app/components/header/header.component.ts b/src/app/components/header/header.component.ts index 58572d6..059c3b3 100644 --- a/src/app/components/header/header.component.ts +++ b/src/app/components/header/header.component.ts @@ -22,6 +22,7 @@ export class HeaderComponent { HeaderType = HeaderType; type = input(HeaderType.LogoWithUserAccountButton); + customAction = input<(() => boolean) | null>(null); readonly buttonColorSchemeMap: Partial< Record diff --git a/src/app/components/top-up/calculator/top-up-calculator.component.html b/src/app/components/top-up/calculator/top-up-calculator.component.html new file mode 100644 index 0000000..b1f09b0 --- /dev/null +++ b/src/app/components/top-up/calculator/top-up-calculator.component.html @@ -0,0 +1,49 @@ +
+
+ R$ + + + @if (topUpValue.touched && topUpValue.hasError('lowTopUpValue')) { +

+ O valor da recarga deve ser no mínimo R$ 1,00 +

+ } @else if (topUpValue.touched && topUpValue.hasError('highTopUpValue')) { +

+ O valor da recarga deve ser no máximo R$ 100,00 +

+ } +
+
+
+ +
+
+ +
diff --git a/src/app/components/top-up/calculator/top-up-calculator.component.ts b/src/app/components/top-up/calculator/top-up-calculator.component.ts new file mode 100644 index 0000000..097045a --- /dev/null +++ b/src/app/components/top-up/calculator/top-up-calculator.component.ts @@ -0,0 +1,87 @@ +import { NgIf } from '@angular/common'; +import { + ChangeDetectionStrategy, + Component, + model, + output, + signal, +} from '@angular/core'; +import { + AbstractControl, + FormControl, + FormsModule, + ReactiveFormsModule, + ValidationErrors, + ValidatorFn, + Validators, +} from '@angular/forms'; + +import { NgxCurrencyDirective, NgxCurrencyInputMode } from 'ngx-currency'; + +import { MealBalanceBreakdownComponent } from '@rusbe/components/meal-balance-breakdown/meal-balance-breakdown.component'; +import { WarningCardComponent } from '@rusbe/components/warning-card/warning-card.component'; + +@Component({ + selector: 'rusbe-top-up-calculator', + imports: [ + NgIf, + MealBalanceBreakdownComponent, + FormsModule, + NgxCurrencyDirective, + ReactiveFormsModule, + WarningCardComponent, + ], + templateUrl: './top-up-calculator.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + class: 'flex flex-grow flex-col items-start justify-start', + }, +}) +export class TopUpCalculatorComponent { + submitted = output(); + + inputDisabled = signal(false); + value = model.required(); + + readonly topUpValue = new FormControl( + { value: '0', disabled: this.inputDisabled() }, + { + validators: [Validators.required, lowTopUpValue, highTopUpValue], + }, + ); + + readonly maskConfig = { + align: 'left', + allowNegative: true, + allowZero: true, + decimal: ',', + precision: 2, + prefix: '', + suffix: '', + thousands: '', + nullable: true, + min: null, + max: null, + inputMode: NgxCurrencyInputMode.Financial, + }; + + onSubmitValue() { + this.topUpValue.markAsTouched(); + if (this.topUpValue.invalid) return; + this.submitted.emit(true); + } +} + +const lowTopUpValue: ValidatorFn = ( + control: AbstractControl, +): ValidationErrors | null => { + const value = control.value; + return value < 1 ? { lowTopUpValue: true } : null; +}; + +const highTopUpValue: ValidatorFn = ( + control: AbstractControl, +): ValidationErrors | null => { + const value = control.value; + return value > 100 ? { highTopUpValue: true } : null; +}; diff --git a/src/app/components/top-up/credit-card/top-up-credit-card.component.html b/src/app/components/top-up/credit-card/top-up-credit-card.component.html new file mode 100644 index 0000000..6859037 --- /dev/null +++ b/src/app/components/top-up/credit-card/top-up-credit-card.component.html @@ -0,0 +1,2 @@ +

top-up-credit-card works!

+, diff --git a/src/app/components/top-up/credit-card/top-up-credit-card.component.ts b/src/app/components/top-up/credit-card/top-up-credit-card.component.ts new file mode 100644 index 0000000..0d49a93 --- /dev/null +++ b/src/app/components/top-up/credit-card/top-up-credit-card.component.ts @@ -0,0 +1,9 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; + +@Component({ + selector: 'rusbe-top-up-credit-card', + imports: [], + templateUrl: './top-up-credit-card.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class TopUpCreditCardComponent {} diff --git a/src/app/components/top-up/in-loco-helper/top-up-in-loco-helper.component.html b/src/app/components/top-up/in-loco-helper/top-up-in-loco-helper.component.html new file mode 100644 index 0000000..0bfe43b --- /dev/null +++ b/src/app/components/top-up/in-loco-helper/top-up-in-loco-helper.component.html @@ -0,0 +1,46 @@ +
+
+
+ +
+
+

Dados pessoais

+

+ {{ parsedCpf() }} +

+

+ {{ name() }} +

+
+
+

Valor da Recarga

+
+

R$

+

+ {{ parsedValue() }} +

+
+
+
+

Forma de pagamento

+
+

+ {{ paymentMethod() }} +

+
+
+
+ +
diff --git a/src/app/components/top-up/in-loco-helper/top-up-in-loco-helper.component.ts b/src/app/components/top-up/in-loco-helper/top-up-in-loco-helper.component.ts new file mode 100644 index 0000000..971c652 --- /dev/null +++ b/src/app/components/top-up/in-loco-helper/top-up-in-loco-helper.component.ts @@ -0,0 +1,49 @@ +import { + ChangeDetectionStrategy, + Component, + computed, + input, + output, +} from '@angular/core'; + +import { NgIcon, provideIcons } from '@ng-icons/core'; +import { lucideMoveRight } from '@ng-icons/lucide'; + +import { LogoComponent } from '@rusbe/components/logo/logo.component'; + +import { PaymentMethods } from '../payment-method/payment-method.component'; + +@Component({ + selector: 'rusbe-top-up-in-loco-helper', + imports: [LogoComponent, NgIcon], + templateUrl: './top-up-in-loco-helper.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + viewProviders: [ + provideIcons({ + lucideMoveRight, + }), + ], + host: { + class: 'flex flex-grow flex-col items-start justify-start', + }, +}) +export class TopUpInLocoHelperComponent { + backClicked = output(); + + cpf = input.required(); + name = input.required(); + topUpValue = input.required(); + paymentMethod = input.required(); + + parsedCpf = computed(() => { + const cpfNumber = this.cpf(); + if (!cpfNumber) return ''; + return cpfNumber.replace(/(\d{3})(\d{3})(\d{3})(\d{2})/, '$1.$2.$3-$4'); + }); + + parsedValue = computed(() => parseFloat(this.topUpValue()).toFixed(2)); + + onBackClicked(): void { + this.backClicked.emit(); + } +} diff --git a/src/app/components/top-up/payment-method/payment-method.component.html b/src/app/components/top-up/payment-method/payment-method.component.html new file mode 100644 index 0000000..ced4273 --- /dev/null +++ b/src/app/components/top-up/payment-method/payment-method.component.html @@ -0,0 +1,37 @@ +
+ @if (true) { +
+ @for (paymentMethod of inAppPaymentMethods; track $index) { + + } +
+ +
+ @for (paymentMethod of inLocoPaymentMethods; track $index) { + + } +
+ } @else { +
+ +

Um momento...

+
+ } +
diff --git a/src/app/components/top-up/payment-method/payment-method.component.ts b/src/app/components/top-up/payment-method/payment-method.component.ts new file mode 100644 index 0000000..53172f2 --- /dev/null +++ b/src/app/components/top-up/payment-method/payment-method.component.ts @@ -0,0 +1,80 @@ +import { ChangeDetectionStrategy, Component, output } from '@angular/core'; + +import { provideIcons } from '@ng-icons/core'; +import { + lucideBanknote, + lucideChevronRight, + lucideCreditCard, + lucideEraser, + lucideFullscreen, + lucideListStart, + lucidePalette, + lucideRotateCcw, + lucideUtensils, +} from '@ng-icons/lucide'; + +import { CardButtonComponent } from '@rusbe/components/cards/card-button/card-button.component'; +import { CardGroupComponent } from '@rusbe/components/cards/card-group/card-group.component'; +import { SpinnerComponent } from '@rusbe/components/spinner/spinner.component'; +import { GeneralGoodsTransactionType } from '@rusbe/services/general-goods/general-goods.service'; + +@Component({ + selector: 'rusbe-top-up-payment-method', + imports: [CardGroupComponent, CardButtonComponent, SpinnerComponent], + templateUrl: './payment-method.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + viewProviders: [ + provideIcons({ + lucidePalette, + lucideUtensils, + lucideListStart, + lucideFullscreen, + lucideRotateCcw, + lucideEraser, + lucideChevronRight, + lucideCreditCard, + lucideBanknote, + }), + ], +}) +export class TopUpPaymentMethodComponent { + paymentMethodChanged = output(); + paymentLocationChanged = output(); + + readonly inAppPaymentMethods = Object.values(GeneralGoodsTransactionType); + readonly inLocoPaymentMethods = INLOCO_PAYMENT_METHODS; + readonly PAYMENT_METHOD_MESSAGES = { + [GeneralGoodsTransactionType.Pix]: + 'Use o código gerado para pagar usando Pix.', + [GeneralGoodsTransactionType.CreditCard]: + 'Use seu cartão de crédito para completar a transação.', + }; + + readonly PAYMENT_METHOD_ICONS = { + [GeneralGoodsTransactionType.Pix]: 'pix', + [GeneralGoodsTransactionType.CreditCard]: 'bootstrapCreditCard', + 'Em espécie': 'bootstrapCash', + 'Cartão de débito': 'bootstrapDebitCard', + }; + + handlePaymentMethodSelection( + selectedPaymentMethod: PaymentMethods, + selectedPaymentLocation: PaymentLocation, + ) { + this.paymentMethodChanged.emit(selectedPaymentMethod); + this.paymentLocationChanged.emit(selectedPaymentLocation); + } +} + +export type PaymentLocation = 'InLoco' | 'InApp'; + +export const INLOCO_PAYMENT_METHODS = [ + 'Em espécie', + 'Cartão de crédito', + 'Cartão de débito', + 'Pix', +] as const; + +export type PaymentMethods = + | (typeof INLOCO_PAYMENT_METHODS)[number] + | GeneralGoodsTransactionType; diff --git a/src/app/components/top-up/pix/top-up-pix.component.html b/src/app/components/top-up/pix/top-up-pix.component.html new file mode 100644 index 0000000..9234949 --- /dev/null +++ b/src/app/components/top-up/pix/top-up-pix.component.html @@ -0,0 +1,104 @@ +@if (false) { +
+
+
+
+
+

R$

+

+ {{ parsedTopUpValue() }} +

+
+
+ +

+ {{ pixCode() }} +

+ +
+ + +
+ +

+ Realize o pagamento usando o app do banco ou instituição financeira de + sua preferência. A transação pode levar alguns minutos para ser + processada. +

+ +
+ +

+ {{ formatedDate }} +

+
+
+
+ +
+
+

+ {{ parsedCpf() }} +

+
+
+

+ {{ name() }} +

+
+
+
+
+
+

+ Sempre verifique se o destinatário do seu pagamento corresponde à + General Goods. +

+

+ Esta transação é feita diretamente à General Goods, e o Rusbé não + recebe, processa ou armazena dados de pagamento. A General Goods é + responsável por processar seu pagamento e pelas informações de créditos + e recarga mostradas. +

+
+
+} @else { +
+ +

Gerando código pix...

+
+} diff --git a/src/app/components/top-up/pix/top-up-pix.component.ts b/src/app/components/top-up/pix/top-up-pix.component.ts new file mode 100644 index 0000000..853dc3d --- /dev/null +++ b/src/app/components/top-up/pix/top-up-pix.component.ts @@ -0,0 +1,52 @@ +import { + ChangeDetectionStrategy, + Component, + computed, + input, +} from '@angular/core'; + +import { NgIcon, provideIcons } from '@ng-icons/core'; +import { + lucideCalendarDays, + lucideCopy, + lucideScanQrCode, + lucideUserRound, +} from '@ng-icons/lucide'; + +import { CardGroupComponent } from '@rusbe/components/cards/card-group/card-group.component'; +import { SpinnerComponent } from '@rusbe/components/spinner/spinner.component'; + +@Component({ + selector: 'rusbe-top-up-pix', + imports: [CardGroupComponent, NgIcon, SpinnerComponent], + templateUrl: './top-up-pix.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + viewProviders: [ + provideIcons({ + lucideCopy, + lucideScanQrCode, + lucideCalendarDays, + lucideUserRound, + }), + ], + host: { + class: 'flex flex-grow flex-col items-start justify-start', + }, +}) +export class TopUpPixComponent { + topUpValue = input.required(); + pixCode = input.required(); + cpf = input.required(); + name = input.required(); + + parsedTopUpValue = computed(() => parseFloat(this.topUpValue()).toFixed(2)); + + parsedCpf = computed(() => { + const cpfNumber = this.cpf(); + if (!cpfNumber) return ''; + return cpfNumber.replace(/(\d{3})(\d{3})(\d{3})(\d{2})/, '•••.•••.$3-$4'); + }); + + readonly today = new Date(); + readonly formatedDate = `${this.today.toLocaleDateString('pt-BR', { day: '2-digit', month: 'long', year: 'numeric' })}, ${Intl.DateTimeFormat('pt-BR', { hour: 'numeric', minute: 'numeric' }).format(this.today)}`; +} diff --git a/src/app/pages/home/home.component.html b/src/app/pages/home/home.component.html index 8dc97d4..3efb33a 100644 --- a/src/app/pages/home/home.component.html +++ b/src/app/pages/home/home.component.html @@ -27,13 +27,10 @@ } @else { + Adicionar créditos } - Adicionar créditos Histórico de cardápios diff --git a/src/app/pages/top-up/top-up.component.html b/src/app/pages/top-up/top-up.component.html index ddae4e4..f578777 100644 --- a/src/app/pages/top-up/top-up.component.html +++ b/src/app/pages/top-up/top-up.component.html @@ -1,42 +1,51 @@ -
-
- ADICIONAR CRÉDITOS -
- -
-
- @if (true) { -
- @for (transactionType of appTransactionTypes; track $index) { - - } -
- -
- @for (transactionType of inLocoTransactionTypes; track $index) { - - } -
- } @else { -
- -

Um momento...

-
- } -
-
-
+
+
+
+ + ADICIONAR CRÉDITOS + +
+ + @if (STAGE_MESSAGE[currentStage()]) { +
+

+ {{ STAGE_MESSAGE[currentStage()] }} +

+
+ } + +
+ @if (!valueSubmitted()) { + + } @else if (!paymentLocation() && !paymentMethod()) { + + } @else if (paymentLocation() !== 'InApp' && paymentMethod()) { + + } @else if (paymentMethod() === 'Pix') { + + } @else if (paymentMethod() === 'Cartão de crédito') { + + } +
+
+
diff --git a/src/app/pages/top-up/top-up.component.ts b/src/app/pages/top-up/top-up.component.ts index 2f30fa6..a0ae923 100644 --- a/src/app/pages/top-up/top-up.component.ts +++ b/src/app/pages/top-up/top-up.component.ts @@ -1,55 +1,130 @@ -import { ChangeDetectionStrategy, Component, signal } from '@angular/core'; - -import { NgIcon, provideIcons } from '@ng-icons/core'; +import { CommonModule } from '@angular/common'; import { - lucideChevronRight, - lucideEraser, - lucideFullscreen, - lucideListStart, - lucidePalette, - lucideRotateCcw, - lucideUtensils, -} from '@ng-icons/lucide'; - -import { CardButtonComponent } from '@rusbe/components/cards/card-button/card-button.component'; -import { CardGroupComponent } from '@rusbe/components/cards/card-group/card-group.component'; + ChangeDetectionStrategy, + Component, + computed, + inject, + signal, + viewChild, +} from '@angular/core'; +import { Router } from '@angular/router'; + import { HeaderComponent, HeaderType, } from '@rusbe/components/header/header.component'; - -import { GeneralGoodsTransactionType } from './../../services/general-goods/general-goods.service'; +import { TopUpCalculatorComponent } from '@rusbe/components/top-up/calculator/top-up-calculator.component'; +import { TopUpCreditCardComponent } from '@rusbe/components/top-up/credit-card/top-up-credit-card.component'; +import { TopUpInLocoHelperComponent } from '@rusbe/components/top-up/in-loco-helper/top-up-in-loco-helper.component'; +import { + PaymentLocation, + PaymentMethods, + TopUpPaymentMethodComponent, +} from '@rusbe/components/top-up/payment-method/payment-method.component'; +import { TopUpPixComponent } from '@rusbe/components/top-up/pix/top-up-pix.component'; +import { AuthStateService } from '@rusbe/services/auth-state/auth-state.service'; @Component({ selector: 'rusbe-top-up', - imports: [HeaderComponent, CardGroupComponent, CardButtonComponent, NgIcon], + imports: [ + HeaderComponent, + CommonModule, + TopUpPaymentMethodComponent, + TopUpCalculatorComponent, + TopUpPixComponent, + TopUpCreditCardComponent, + TopUpInLocoHelperComponent, + ], templateUrl: './top-up.component.html', changeDetection: ChangeDetectionStrategy.OnPush, - viewProviders: [ - provideIcons({ - lucidePalette, - lucideUtensils, - lucideListStart, - lucideFullscreen, - lucideRotateCcw, - lucideEraser, - lucideChevronRight, - }), - ], }) export class TopUpComponent { readonly HEADER_TYPE = HeaderType.PageNameWithBackButton; - appTransactionTypes = Object.values(GeneralGoodsTransactionType); - inLocoTransactionTypes = [ - 'Em espécie', - 'Cartão de crédito', - 'Cartão de débito', - 'Pix', - ]; - TRANSACTION_TYPE_MESSAGES = { - [GeneralGoodsTransactionType.Pix]: - 'Use o código gerado para pagar usando Pix.', - [GeneralGoodsTransactionType.CreditCard]: - 'Use seu cartão de crédito para completar a transação.', + readonly STAGE_MESSAGE = { + [TopUpStages.Calculator]: 'Quanto você quer adicionar?', + [TopUpStages.PaymentMethod]: 'Como você deseja adicionar créditos?', + [TopUpStages.InLocoHelper]: 'No guichê, mostre essas informações', + [TopUpStages.Pix]: '', + [TopUpStages.CreditCard]: '', }; + + calculatorComponent = viewChild(TopUpCalculatorComponent); + paymentMethodComponent = viewChild(TopUpPaymentMethodComponent); + inLocoHelperComponent = viewChild(TopUpInLocoHelperComponent); + creditCardComponent = viewChild(TopUpCreditCardComponent); + pixComponent = viewChild(TopUpPixComponent); + + currentStage = computed(() => { + if (this.calculatorComponent()) { + return 'calculator'; + } else if (this.paymentMethodComponent()) { + return 'payment-method'; + } else if (this.inLocoHelperComponent()) { + return 'in-loco-helper'; + } else if (this.creditCardComponent()) { + return 'credit-card'; + } else if (this.pixComponent()) { + return 'pix'; + } + return 'calculator'; + }); + + valueSubmitted = signal(false); + value = signal('0'); + paymentMethod = signal(null); + paymentLocation = signal(null); + pixCode = + '00020126580014br.gov.bcb.pix0136bee05743-4291-4f3c-9259-595df1307ba1520400005303986540510.005802BR5914AlexandreLima6019Presidente Prudente62180514Um-Id-Qualquer6304D475'; + + name = computed( + () => this.authStateService.generalGoodsAccountData()?.fullName ?? '', + ); + + private readonly router = inject(Router); + private readonly authStateService = inject(AuthStateService); + cpf = computed( + () => this.authStateService.generalGoodsAccountData()?.cpfNumber ?? '', + ); + + goToPreviousStage(): boolean { + switch (this.currentStage()) { + case 'calculator': + return false; + case 'payment-method': + this.valueSubmitted.set(false); + return true; + case 'credit-card': + case 'in-loco-helper': + case 'pix': + this.paymentLocation.set(null); + this.paymentMethod.set(null); + return true; + default: + return false; + } + } + + goToHome() { + this.router.navigate(['/']); + } + + onPaymentMethodChange(paymentMethod: PaymentMethods) { + this.paymentMethod.set(paymentMethod); + } + + onPaymentLocaltionChange(paymentLocation: PaymentLocation) { + this.paymentLocation.set(paymentLocation); + } + + onCalculatorConfirm(value: boolean) { + this.valueSubmitted.set(value); + } +} + +export enum TopUpStages { + Calculator = 'calculator', + PaymentMethod = 'payment-method', + Pix = 'pix', + CreditCard = 'credit-card', + InLocoHelper = 'in-loco-helper', } diff --git a/src/styles.css b/src/styles.css index 6c10808..b0c931c 100644 --- a/src/styles.css +++ b/src/styles.css @@ -199,6 +199,10 @@ @apply w-full justify-center; } + .button-split { + @apply button-base w-full justify-between; + } + .button-large { @apply p-4 text-sm sm:text-lg; } From 30813f2cf88ff36e89f20baca8ee7e695d4c0029 Mon Sep 17 00:00:00 2001 From: Guilherme Afonso Date: Fri, 28 Feb 2025 21:12:02 -0300 Subject: [PATCH 04/25] feat: pix transaction data --- .../top-up/pix/top-up-pix.component.html | 4 ++-- .../top-up/pix/top-up-pix.component.ts | 4 +++- src/app/pages/top-up/top-up.component.html | 2 +- src/app/pages/top-up/top-up.component.ts | 20 +++++++++++++++++++ 4 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/app/components/top-up/pix/top-up-pix.component.html b/src/app/components/top-up/pix/top-up-pix.component.html index 9234949..0f9b7fa 100644 --- a/src/app/components/top-up/pix/top-up-pix.component.html +++ b/src/app/components/top-up/pix/top-up-pix.component.html @@ -1,4 +1,4 @@ -@if (false) { +@if (pixTransactionData()) {
@@ -12,7 +12,7 @@

- {{ pixCode() }} + {{ pixTransactionData()?.qrCodeString }}

diff --git a/src/app/components/top-up/pix/top-up-pix.component.ts b/src/app/components/top-up/pix/top-up-pix.component.ts index 853dc3d..ac70f91 100644 --- a/src/app/components/top-up/pix/top-up-pix.component.ts +++ b/src/app/components/top-up/pix/top-up-pix.component.ts @@ -15,6 +15,7 @@ import { import { CardGroupComponent } from '@rusbe/components/cards/card-group/card-group.component'; import { SpinnerComponent } from '@rusbe/components/spinner/spinner.component'; +import { GeneralGoodsPixTransactionData } from '@rusbe/services/general-goods/general-goods.service'; @Component({ selector: 'rusbe-top-up-pix', @@ -35,10 +36,11 @@ import { SpinnerComponent } from '@rusbe/components/spinner/spinner.component'; }) export class TopUpPixComponent { topUpValue = input.required(); - pixCode = input.required(); cpf = input.required(); name = input.required(); + pixTransactionData = input(); + parsedTopUpValue = computed(() => parseFloat(this.topUpValue()).toFixed(2)); parsedCpf = computed(() => { diff --git a/src/app/pages/top-up/top-up.component.html b/src/app/pages/top-up/top-up.component.html index f578777..487cba0 100644 --- a/src/app/pages/top-up/top-up.component.html +++ b/src/app/pages/top-up/top-up.component.html @@ -41,7 +41,7 @@ [cpf]="cpf()" [name]="name()" [topUpValue]="value()" - [pixCode]="pixCode" + [pixTransactionData]="pixTransactionData()" > } @else if (paymentMethod() === 'Cartão de crédito') { diff --git a/src/app/pages/top-up/top-up.component.ts b/src/app/pages/top-up/top-up.component.ts index a0ae923..148a9fa 100644 --- a/src/app/pages/top-up/top-up.component.ts +++ b/src/app/pages/top-up/top-up.component.ts @@ -3,6 +3,7 @@ import { ChangeDetectionStrategy, Component, computed, + effect, inject, signal, viewChild, @@ -23,6 +24,11 @@ import { } from '@rusbe/components/top-up/payment-method/payment-method.component'; import { TopUpPixComponent } from '@rusbe/components/top-up/pix/top-up-pix.component'; import { AuthStateService } from '@rusbe/services/auth-state/auth-state.service'; +import { + GeneralGoodsPixTransactionData, + GeneralGoodsService, +} from '@rusbe/services/general-goods/general-goods.service'; +import { BrlCurrency } from '@rusbe/types/brl-currency'; @Component({ selector: 'rusbe-top-up', @@ -75,17 +81,31 @@ export class TopUpComponent { paymentLocation = signal(null); pixCode = '00020126580014br.gov.bcb.pix0136bee05743-4291-4f3c-9259-595df1307ba1520400005303986540510.005802BR5914AlexandreLima6019Presidente Prudente62180514Um-Id-Qualquer6304D475'; + pixTransactionData = signal(null); name = computed( () => this.authStateService.generalGoodsAccountData()?.fullName ?? '', ); private readonly router = inject(Router); + private readonly generalGoodsService = inject(GeneralGoodsService); private readonly authStateService = inject(AuthStateService); cpf = computed( () => this.authStateService.generalGoodsAccountData()?.cpfNumber ?? '', ); + constructor() { + effect(async () => { + if (this.pixComponent()) { + const pixData = + await this.generalGoodsService.startAddCreditsTransactionUsingPix( + BrlCurrency.fromNumber(parseFloat(this.value())), + ); + this.pixTransactionData.set(pixData); + } + }); + } + goToPreviousStage(): boolean { switch (this.currentStage()) { case 'calculator': From 6fed8035883b43da275b6891f7fec538d68c0600 Mon Sep 17 00:00:00 2001 From: Guilherme Afonso Date: Tue, 4 Mar 2025 03:26:56 -0300 Subject: [PATCH 05/25] feat: add error handling and pix timer --- src/app/app.routes.ts | 2 +- .../card-button/card-button.component.html | 7 ++ .../card-button/card-button.component.ts | 1 + .../payment-method.component.html | 1 + .../top-up/pix/top-up-pix.component.html | 45 ++++++++- .../top-up/pix/top-up-pix.component.ts | 15 ++- src/app/pages/home/home.component.html | 6 +- src/app/pages/top-up/top-up.component.html | 23 +++-- src/app/pages/top-up/top-up.component.ts | 98 ++++++++++++++----- 9 files changed, 162 insertions(+), 36 deletions(-) diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index e848e42..0cf88c1 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -88,7 +88,7 @@ export const routes: Routes = [ path: 'top-up', loadComponent: () => import('./pages/top-up/top-up.component').then((m) => m.TopUpComponent), - ...canActivate(redirectUnauthorizedToLogin), + ...canActivate(redirectFirebaseUnauthorizedToLogin), }, { path: '404', diff --git a/src/app/components/cards/card-button/card-button.component.html b/src/app/components/cards/card-button/card-button.component.html index c245e7d..0f5d132 100644 --- a/src/app/components/cards/card-button/card-button.component.html +++ b/src/app/components/cards/card-button/card-button.component.html @@ -14,6 +14,13 @@

{{ subtitle() }}

+ @if (badgeText()) { +

+ {{ badgeText() }} +

+ } diff --git a/src/app/components/top-up/pix/top-up-pix.component.html b/src/app/components/top-up/pix/top-up-pix.component.html index 0f9b7fa..a80f0c8 100644 --- a/src/app/components/top-up/pix/top-up-pix.component.html +++ b/src/app/components/top-up/pix/top-up-pix.component.html @@ -1,4 +1,30 @@ -@if (pixTransactionData()) { +@if (pixErrorMessage()) { +
+
+ +

{{ pixErrorMessage() }}

+
+ + +
+} @else if (pixTransactionData()) {
@@ -15,6 +41,10 @@ {{ pixTransactionData()?.qrCodeString }}

+

+ Tempo restante para pagamento: {{ timeLeft() }} +

+
+ @if (showPixQrCode()) { + Código QR Pix + } +

Realize o pagamento usando o app do banco ou instituição financeira de sua preferência. A transação pode levar alguns minutos para ser diff --git a/src/app/components/top-up/pix/top-up-pix.component.ts b/src/app/components/top-up/pix/top-up-pix.component.ts index ac70f91..38c3454 100644 --- a/src/app/components/top-up/pix/top-up-pix.component.ts +++ b/src/app/components/top-up/pix/top-up-pix.component.ts @@ -3,12 +3,15 @@ import { Component, computed, input, + signal, } from '@angular/core'; import { NgIcon, provideIcons } from '@ng-icons/core'; import { lucideCalendarDays, lucideCopy, + lucideInfo, + lucideMoveRight, lucideScanQrCode, lucideUserRound, } from '@ng-icons/lucide'; @@ -28,6 +31,8 @@ import { GeneralGoodsPixTransactionData } from '@rusbe/services/general-goods/ge lucideScanQrCode, lucideCalendarDays, lucideUserRound, + lucideMoveRight, + lucideInfo, }), ], host: { @@ -38,17 +43,25 @@ export class TopUpPixComponent { topUpValue = input.required(); cpf = input.required(); name = input.required(); + timeLeft = input.required(); pixTransactionData = input(); + pixErrorMessage = input(''); + pixQrCodeSource = computed(() => { + const pixTransactionData = this.pixTransactionData(); + if (!pixTransactionData) return ''; + return `data:image/png;base64, ${pixTransactionData.qrCodeBase64Image}`; + }); parsedTopUpValue = computed(() => parseFloat(this.topUpValue()).toFixed(2)); - parsedCpf = computed(() => { const cpfNumber = this.cpf(); if (!cpfNumber) return ''; return cpfNumber.replace(/(\d{3})(\d{3})(\d{3})(\d{2})/, '•••.•••.$3-$4'); }); + showPixQrCode = signal(false); + readonly today = new Date(); readonly formatedDate = `${this.today.toLocaleDateString('pt-BR', { day: '2-digit', month: 'long', year: 'numeric' })}, ${Intl.DateTimeFormat('pt-BR', { hour: 'numeric', minute: 'numeric' }).format(this.today)}`; } diff --git a/src/app/pages/home/home.component.html b/src/app/pages/home/home.component.html index 3efb33a..92530ad 100644 --- a/src/app/pages/home/home.component.html +++ b/src/app/pages/home/home.component.html @@ -27,10 +27,10 @@ } @else { - Adicionar créditos } + Adicionar créditos Histórico de cardápios diff --git a/src/app/pages/top-up/top-up.component.html b/src/app/pages/top-up/top-up.component.html index 487cba0..4fa69e5 100644 --- a/src/app/pages/top-up/top-up.component.html +++ b/src/app/pages/top-up/top-up.component.html @@ -9,7 +9,7 @@

- @if (STAGE_MESSAGE[currentStage()]) { + @if (!topUpUnavailable() && STAGE_MESSAGE[currentStage()]) {

{{ STAGE_MESSAGE[currentStage()] }} @@ -18,7 +18,16 @@ }

- @if (!valueSubmitted()) { + @if (topUpUnavailable()) { +
+

+ Infelizmente, o sistema da General Goods está fora do ar. +

+

Por favor, tente novamente mais tarde.

+
+ } @else if (!valueSubmitted()) { } @else if (paymentLocation() !== 'InApp' && paymentMethod()) { } @else if (paymentMethod() === 'Pix') { } @else if (paymentMethod() === 'Cartão de crédito') { diff --git a/src/app/pages/top-up/top-up.component.ts b/src/app/pages/top-up/top-up.component.ts index 148a9fa..b62c6b2 100644 --- a/src/app/pages/top-up/top-up.component.ts +++ b/src/app/pages/top-up/top-up.component.ts @@ -10,6 +10,8 @@ import { } from '@angular/core'; import { Router } from '@angular/router'; +import { interval, takeUntil, timer } from 'rxjs'; + import { HeaderComponent, HeaderType, @@ -23,12 +25,16 @@ import { TopUpPaymentMethodComponent, } from '@rusbe/components/top-up/payment-method/payment-method.component'; import { TopUpPixComponent } from '@rusbe/components/top-up/pix/top-up-pix.component'; -import { AuthStateService } from '@rusbe/services/auth-state/auth-state.service'; +import { + AccountAuthState, + AuthStateService, +} from '@rusbe/services/auth-state/auth-state.service'; import { GeneralGoodsPixTransactionData, GeneralGoodsService, } from '@rusbe/services/general-goods/general-goods.service'; import { BrlCurrency } from '@rusbe/types/brl-currency'; +import { RusbeError } from '@rusbe/types/error-handling'; @Component({ selector: 'rusbe-top-up', @@ -53,6 +59,7 @@ export class TopUpComponent { [TopUpStages.Pix]: '', [TopUpStages.CreditCard]: '', }; + readonly FIFHTEEEN_MINUTES = 15 * 1000; calculatorComponent = viewChild(TopUpCalculatorComponent); paymentMethodComponent = viewChild(TopUpPaymentMethodComponent); @@ -60,6 +67,24 @@ export class TopUpComponent { creditCardComponent = viewChild(TopUpCreditCardComponent); pixComponent = viewChild(TopUpPixComponent); + valueSubmitted = signal(false); + value = signal('0'); + paymentMethod = signal(null); + paymentLocation = signal(null); + pixTransactionData = signal(null); + pixErrorMessage = signal(''); + remainingTimeString = signal(this.parseRemainingTime(0)); + + accountData = computed(() => { + const accountData = this.authStateService.generalGoodsAccountData(); + if (!accountData) return { fullName: '', cpfNumber: '' }; + + return { + fullName: accountData.fullName, + cpfNumber: accountData.cpfNumber, + }; + }); + currentStage = computed(() => { if (this.calculatorComponent()) { return 'calculator'; @@ -75,34 +100,19 @@ export class TopUpComponent { return 'calculator'; }); - valueSubmitted = signal(false); - value = signal('0'); - paymentMethod = signal(null); - paymentLocation = signal(null); - pixCode = - '00020126580014br.gov.bcb.pix0136bee05743-4291-4f3c-9259-595df1307ba1520400005303986540510.005802BR5914AlexandreLima6019Presidente Prudente62180514Um-Id-Qualquer6304D475'; - pixTransactionData = signal(null); + topUpUnavailable = computed(() => { + const accountAuthState = this.authStateService.accountAuthState(); - name = computed( - () => this.authStateService.generalGoodsAccountData()?.fullName ?? '', - ); + return accountAuthState === AccountAuthState.GeneralGoodsServiceUnavailable; + }); private readonly router = inject(Router); private readonly generalGoodsService = inject(GeneralGoodsService); private readonly authStateService = inject(AuthStateService); - cpf = computed( - () => this.authStateService.generalGoodsAccountData()?.cpfNumber ?? '', - ); constructor() { - effect(async () => { - if (this.pixComponent()) { - const pixData = - await this.generalGoodsService.startAddCreditsTransactionUsingPix( - BrlCurrency.fromNumber(parseFloat(this.value())), - ); - this.pixTransactionData.set(pixData); - } + effect(() => { + if (this.pixComponent()) this.startPixTransaction(); }); } @@ -139,6 +149,50 @@ export class TopUpComponent { onCalculatorConfirm(value: boolean) { this.valueSubmitted.set(value); } + + private async startPixTransaction() { + try { + const pixTransactionResponse = + await this.generalGoodsService.startAddCreditsTransactionUsingPix( + BrlCurrency.fromNumber(parseFloat(this.value())), + ); + this.pixTransactionData.set(pixTransactionResponse); + + this.startPixTimer(); + } catch (error) { + if (error instanceof RusbeError) { + this.pixErrorMessage.set( + 'Ocorreu um erro ao tentar gerar o código pix.', + ); + return; + } + this.pixErrorMessage.set( + 'Ocorreu um erro desconhecido. Por favor, tente novamente.', + ); + } + } + + private startPixTimer() { + const source = interval(1000); + + const result = source.pipe(takeUntil(timer(this.FIFHTEEEN_MINUTES))); + + result.subscribe({ + next: (timeSpent) => { + this.remainingTimeString.set(this.parseRemainingTime(timeSpent)); + }, + complete: () => { + this.router.navigate(['/']); + }, + }); + } + + private parseRemainingTime(timeSpent: number): string { + const remainingTime = this.FIFHTEEEN_MINUTES - timeSpent * 1000; + const minutes = Math.floor(remainingTime / 60000); + const seconds = Math.floor((remainingTime % 60000) / 1000); + return `${minutes}:${seconds < 10 ? '0' : ''}${seconds}`; + } } export enum TopUpStages { From e540bab08673fa5f7e6c9cd04170a3d47d8237ad Mon Sep 17 00:00:00 2001 From: Guilherme Afonso Date: Tue, 4 Mar 2025 03:30:29 -0300 Subject: [PATCH 06/25] fix: default timeout --- src/app/pages/top-up/top-up.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/pages/top-up/top-up.component.ts b/src/app/pages/top-up/top-up.component.ts index b62c6b2..f4d6b51 100644 --- a/src/app/pages/top-up/top-up.component.ts +++ b/src/app/pages/top-up/top-up.component.ts @@ -59,7 +59,7 @@ export class TopUpComponent { [TopUpStages.Pix]: '', [TopUpStages.CreditCard]: '', }; - readonly FIFHTEEEN_MINUTES = 15 * 1000; + readonly FIFHTEEEN_MINUTES = 15 * 60 * 1000; calculatorComponent = viewChild(TopUpCalculatorComponent); paymentMethodComponent = viewChild(TopUpPaymentMethodComponent); From 1c4e13354b88d9175ceb28d73165a7fc7cd506da Mon Sep 17 00:00:00 2001 From: Guilherme Afonso Date: Tue, 4 Mar 2025 14:03:14 -0300 Subject: [PATCH 07/25] refactor: cleanup of unnecessary code --- .../top-up/credit-card/top-up-credit-card.component.html | 2 -- src/app/pages/top-up/top-up.component.html | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/app/components/top-up/credit-card/top-up-credit-card.component.html b/src/app/components/top-up/credit-card/top-up-credit-card.component.html index 6859037..e69de29 100644 --- a/src/app/components/top-up/credit-card/top-up-credit-card.component.html +++ b/src/app/components/top-up/credit-card/top-up-credit-card.component.html @@ -1,2 +0,0 @@ -

top-up-credit-card works!

-, diff --git a/src/app/pages/top-up/top-up.component.html b/src/app/pages/top-up/top-up.component.html index 4fa69e5..319b987 100644 --- a/src/app/pages/top-up/top-up.component.html +++ b/src/app/pages/top-up/top-up.component.html @@ -37,7 +37,7 @@ (paymentMethodChanged)="onPaymentMethodChange($event)" (paymentLocationChanged)="onPaymentLocaltionChange($event)" > - } @else if (paymentLocation() !== 'InApp' && paymentMethod()) { + } @else if (paymentLocation() === 'InLoco') { Date: Tue, 4 Mar 2025 14:05:16 -0300 Subject: [PATCH 08/25] refactor: remove unused import --- .../components/jumbo-text-input/jumbo-text-input.component.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/components/jumbo-text-input/jumbo-text-input.component.ts b/src/app/components/jumbo-text-input/jumbo-text-input.component.ts index 5317e89..ea64564 100644 --- a/src/app/components/jumbo-text-input/jumbo-text-input.component.ts +++ b/src/app/components/jumbo-text-input/jumbo-text-input.component.ts @@ -1,11 +1,11 @@ import { Component, computed, input, model } from '@angular/core'; import { FormsModule } from '@angular/forms'; -import { NgxMaskDirective, provideNgxMask } from 'ngx-mask'; +import { provideNgxMask } from 'ngx-mask'; @Component({ selector: 'rusbe-jumbo-text-input', - imports: [NgxMaskDirective, FormsModule], + imports: [FormsModule], providers: [provideNgxMask()], templateUrl: './jumbo-text-input.component.html', }) From c59d0b1101377321ff51d0252ebc7eb583e487d9 Mon Sep 17 00:00:00 2001 From: Guilherme Afonso Date: Tue, 4 Mar 2025 14:39:43 -0300 Subject: [PATCH 09/25] refactor: code structure and naming --- .../calculator/top-up-calculator.component.ts | 15 ++++++++++----- .../top-up-in-loco-helper.component.ts | 13 +++++++------ .../top-up/pix/top-up-pix.component.html | 2 +- .../top-up/pix/top-up-pix.component.ts | 17 +++++++++++++++-- 4 files changed, 33 insertions(+), 14 deletions(-) diff --git a/src/app/components/top-up/calculator/top-up-calculator.component.ts b/src/app/components/top-up/calculator/top-up-calculator.component.ts index 097045a..904e266 100644 --- a/src/app/components/top-up/calculator/top-up-calculator.component.ts +++ b/src/app/components/top-up/calculator/top-up-calculator.component.ts @@ -40,19 +40,24 @@ import { WarningCardComponent } from '@rusbe/components/warning-card/warning-car export class TopUpCalculatorComponent { submitted = output(); - inputDisabled = signal(false); value = model.required(); + inputDisabled = signal(false); + readonly topUpValue = new FormControl( { value: '0', disabled: this.inputDisabled() }, { - validators: [Validators.required, lowTopUpValue, highTopUpValue], + validators: [ + Validators.required, + lowTopUpValueValidator, + highTopUpValueValidator, + ], }, ); readonly maskConfig = { align: 'left', - allowNegative: true, + allowNegative: false, allowZero: true, decimal: ',', precision: 2, @@ -72,14 +77,14 @@ export class TopUpCalculatorComponent { } } -const lowTopUpValue: ValidatorFn = ( +const lowTopUpValueValidator: ValidatorFn = ( control: AbstractControl, ): ValidationErrors | null => { const value = control.value; return value < 1 ? { lowTopUpValue: true } : null; }; -const highTopUpValue: ValidatorFn = ( +const highTopUpValueValidator: ValidatorFn = ( control: AbstractControl, ): ValidationErrors | null => { const value = control.value; diff --git a/src/app/components/top-up/in-loco-helper/top-up-in-loco-helper.component.ts b/src/app/components/top-up/in-loco-helper/top-up-in-loco-helper.component.ts index 971c652..e8262c4 100644 --- a/src/app/components/top-up/in-loco-helper/top-up-in-loco-helper.component.ts +++ b/src/app/components/top-up/in-loco-helper/top-up-in-loco-helper.component.ts @@ -35,15 +35,16 @@ export class TopUpInLocoHelperComponent { topUpValue = input.required(); paymentMethod = input.required(); - parsedCpf = computed(() => { - const cpfNumber = this.cpf(); - if (!cpfNumber) return ''; - return cpfNumber.replace(/(\d{3})(\d{3})(\d{3})(\d{2})/, '$1.$2.$3-$4'); - }); - + parsedCpf = computed(this.parseCpfNumber.bind(this)); parsedValue = computed(() => parseFloat(this.topUpValue()).toFixed(2)); onBackClicked(): void { this.backClicked.emit(); } + + private parseCpfNumber(): string { + const cpfNumber = this.cpf(); + if (!cpfNumber) return ''; + return cpfNumber.replace(/(\d{3})(\d{3})(\d{3})(\d{2})/, '$1.$2.$3-$4'); + } } diff --git a/src/app/components/top-up/pix/top-up-pix.component.html b/src/app/components/top-up/pix/top-up-pix.component.html index a80f0c8..9b926d0 100644 --- a/src/app/components/top-up/pix/top-up-pix.component.html +++ b/src/app/components/top-up/pix/top-up-pix.component.html @@ -92,7 +92,7 @@ aria-hidden="true" >

- {{ formatedDate }} + {{ LOCALIZED_CURRENT_DATE }}

(); cpf = input.required(); name = input.required(); @@ -62,6 +65,16 @@ export class TopUpPixComponent { showPixQrCode = signal(false); - readonly today = new Date(); - readonly formatedDate = `${this.today.toLocaleDateString('pt-BR', { day: '2-digit', month: 'long', year: 'numeric' })}, ${Intl.DateTimeFormat('pt-BR', { hour: 'numeric', minute: 'numeric' }).format(this.today)}`; + private parseCurrentDate(): string { + const dateString = this.CURRENT_DATE.toLocaleDateString('pt-BR', { + day: '2-digit', + month: 'long', + year: 'numeric', + }); + const timeString = Intl.DateTimeFormat('pt-BR', { + hour: 'numeric', + minute: 'numeric', + }).format(this.CURRENT_DATE); + return `${dateString}, ${timeString}`; + } } From 4f18b24bf29218c1307e66a529055977cb66e0d3 Mon Sep 17 00:00:00 2001 From: Guilherme Afonso Date: Tue, 4 Mar 2025 14:43:29 -0300 Subject: [PATCH 10/25] refactor: code structure and naming --- .../top-up-in-loco-helper.component.ts | 2 +- .../top-up/pix/top-up-pix.component.ts | 24 +++++++++++-------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/app/components/top-up/in-loco-helper/top-up-in-loco-helper.component.ts b/src/app/components/top-up/in-loco-helper/top-up-in-loco-helper.component.ts index e8262c4..509be5a 100644 --- a/src/app/components/top-up/in-loco-helper/top-up-in-loco-helper.component.ts +++ b/src/app/components/top-up/in-loco-helper/top-up-in-loco-helper.component.ts @@ -35,7 +35,7 @@ export class TopUpInLocoHelperComponent { topUpValue = input.required(); paymentMethod = input.required(); - parsedCpf = computed(this.parseCpfNumber.bind(this)); + parsedCpf = computed(() => this.parseCpfNumber()); parsedValue = computed(() => parseFloat(this.topUpValue()).toFixed(2)); onBackClicked(): void { diff --git a/src/app/components/top-up/pix/top-up-pix.component.ts b/src/app/components/top-up/pix/top-up-pix.component.ts index f057444..ca04867 100644 --- a/src/app/components/top-up/pix/top-up-pix.component.ts +++ b/src/app/components/top-up/pix/top-up-pix.component.ts @@ -51,17 +51,9 @@ export class TopUpPixComponent { pixTransactionData = input(); pixErrorMessage = input(''); - pixQrCodeSource = computed(() => { - const pixTransactionData = this.pixTransactionData(); - if (!pixTransactionData) return ''; - return `data:image/png;base64, ${pixTransactionData.qrCodeBase64Image}`; - }); + pixQrCodeSource = computed(() => this.parseBase64ImageSource()); parsedTopUpValue = computed(() => parseFloat(this.topUpValue()).toFixed(2)); - parsedCpf = computed(() => { - const cpfNumber = this.cpf(); - if (!cpfNumber) return ''; - return cpfNumber.replace(/(\d{3})(\d{3})(\d{3})(\d{2})/, '•••.•••.$3-$4'); - }); + parsedCpf = computed(() => this.parseCpfNumber()); showPixQrCode = signal(false); @@ -77,4 +69,16 @@ export class TopUpPixComponent { }).format(this.CURRENT_DATE); return `${dateString}, ${timeString}`; } + + private parseBase64ImageSource(): string { + const pixTransactionData = this.pixTransactionData(); + if (!pixTransactionData) return ''; + return `data:image/png;base64, ${pixTransactionData.qrCodeBase64Image}`; + } + + private parseCpfNumber(): string { + const cpfNumber = this.cpf(); + if (!cpfNumber) return ''; + return cpfNumber.replace(/(\d{3})(\d{3})(\d{3})(\d{2})/, '•••.•••.$3-$4'); + } } From 35c3ddcc1ae95f979122f738e5e5fab8abdf4ae4 Mon Sep 17 00:00:00 2001 From: Guilherme Afonso Date: Tue, 4 Mar 2025 17:42:28 -0300 Subject: [PATCH 11/25] feat: ui improvements --- .../payment-method.component.html | 61 ++++----- .../payment-method.component.ts | 3 +- .../top-up/pix/top-up-pix.component.html | 69 ++++------ .../top-up/pix/top-up-pix.component.ts | 17 ++- src/app/pages/top-up/top-up.component.html | 118 +++++++++++------- src/app/pages/top-up/top-up.component.ts | 75 ++++++++--- src/styles.css | 2 +- 7 files changed, 198 insertions(+), 147 deletions(-) diff --git a/src/app/components/top-up/payment-method/payment-method.component.html b/src/app/components/top-up/payment-method/payment-method.component.html index 6fc1d7a..e0c9f18 100644 --- a/src/app/components/top-up/payment-method/payment-method.component.html +++ b/src/app/components/top-up/payment-method/payment-method.component.html @@ -1,38 +1,29 @@
- @if (true) { -
- @for (paymentMethod of inAppPaymentMethods; track $index) { - - } -
+
+ @for (paymentMethod of inAppPaymentMethods; track $index) { + + } +
-
- @for (paymentMethod of inLocoPaymentMethods; track $index) { - - } -
- } @else { -
- -

Um momento...

-
- } +
+ @for (paymentMethod of inLocoPaymentMethods; track $index) { + + } +
diff --git a/src/app/components/top-up/payment-method/payment-method.component.ts b/src/app/components/top-up/payment-method/payment-method.component.ts index 53172f2..97d930a 100644 --- a/src/app/components/top-up/payment-method/payment-method.component.ts +++ b/src/app/components/top-up/payment-method/payment-method.component.ts @@ -15,12 +15,11 @@ import { import { CardButtonComponent } from '@rusbe/components/cards/card-button/card-button.component'; import { CardGroupComponent } from '@rusbe/components/cards/card-group/card-group.component'; -import { SpinnerComponent } from '@rusbe/components/spinner/spinner.component'; import { GeneralGoodsTransactionType } from '@rusbe/services/general-goods/general-goods.service'; @Component({ selector: 'rusbe-top-up-payment-method', - imports: [CardGroupComponent, CardButtonComponent, SpinnerComponent], + imports: [CardGroupComponent, CardButtonComponent], templateUrl: './payment-method.component.html', changeDetection: ChangeDetectionStrategy.OnPush, viewProviders: [ diff --git a/src/app/components/top-up/pix/top-up-pix.component.html b/src/app/components/top-up/pix/top-up-pix.component.html index 9b926d0..e6675f1 100644 --- a/src/app/components/top-up/pix/top-up-pix.component.html +++ b/src/app/components/top-up/pix/top-up-pix.component.html @@ -1,30 +1,4 @@ -@if (pixErrorMessage()) { -
-
- -

{{ pixErrorMessage() }}

-
- - -
-} @else if (pixTransactionData()) { +@if (pixTransactionData()) {
@@ -41,21 +15,23 @@ {{ pixTransactionData()?.qrCodeString }}

-

- Tempo restante para pagamento: {{ timeLeft() }} -

-
-

+

{{ parsedCpf() }}

-

- {{ name() }} +

+ {{ name() }}

+

+ Tempo restante para pagamento: {{ timeLeft() }} +

@@ -135,9 +114,11 @@
} @else {
- -

Gerando código pix...

+
+ +
+

Gerando código pix...

} diff --git a/src/app/components/top-up/pix/top-up-pix.component.ts b/src/app/components/top-up/pix/top-up-pix.component.ts index ca04867..75adc1b 100644 --- a/src/app/components/top-up/pix/top-up-pix.component.ts +++ b/src/app/components/top-up/pix/top-up-pix.component.ts @@ -1,7 +1,9 @@ +import { Clipboard } from '@angular/cdk/clipboard'; import { ChangeDetectionStrategy, Component, computed, + inject, input, signal, } from '@angular/core'; @@ -49,13 +51,22 @@ export class TopUpPixComponent { timeLeft = input.required(); pixTransactionData = input(); - pixErrorMessage = input(''); pixQrCodeSource = computed(() => this.parseBase64ImageSource()); parsedTopUpValue = computed(() => parseFloat(this.topUpValue()).toFixed(2)); parsedCpf = computed(() => this.parseCpfNumber()); - showPixQrCode = signal(false); + showPixQrCode = signal(false); + copiedToClipboard = signal(false); + + private readonly clipboard = inject(Clipboard); + + copyPasswordToClipboard() { + const pixTransactionData = this.pixTransactionData(); + if (!pixTransactionData) return; + this.clipboard.copy(pixTransactionData.qrCodeString); + this.copiedToClipboard.set(true); + } private parseCurrentDate(): string { const dateString = this.CURRENT_DATE.toLocaleDateString('pt-BR', { @@ -79,6 +90,6 @@ export class TopUpPixComponent { private parseCpfNumber(): string { const cpfNumber = this.cpf(); if (!cpfNumber) return ''; - return cpfNumber.replace(/(\d{3})(\d{3})(\d{3})(\d{2})/, '•••.•••.$3-$4'); + return cpfNumber.replace(/(\d{3})(\d{3})(\d{3})(\d{2})/, '•••.$2.$3-••'); } } diff --git a/src/app/pages/top-up/top-up.component.html b/src/app/pages/top-up/top-up.component.html index 319b987..f48b9c9 100644 --- a/src/app/pages/top-up/top-up.component.html +++ b/src/app/pages/top-up/top-up.component.html @@ -8,55 +8,89 @@ ADICIONAR CRÉDITOS
+ @let currentErrorValue = currentError(); - @if (!topUpUnavailable() && STAGE_MESSAGE[currentStage()]) { -
-

- {{ STAGE_MESSAGE[currentStage()] }} -

+ @if (accountAuthState() === undefined) { +
+
+ +
+

Um momento...

- } + } @else if (currentErrorValue) { +
+
+ + +

+ {{ ERROR_MESSAGES[currentErrorValue] }} +

+
-
- @if (topUpUnavailable()) {
-

- Infelizmente, o sistema da General Goods está fora do ar. + + +

+
+ } @else { + @if (STAGE_MESSAGE[currentStage()]) { +
+

+ {{ STAGE_MESSAGE[currentStage()] }}

-

Por favor, tente novamente mais tarde.

- } @else if (!valueSubmitted()) { - - } @else if (!paymentLocation() && !paymentMethod()) { - - } @else if (paymentLocation() === 'InLoco') { - - } @else if (paymentMethod() === 'Pix') { - - } @else if (paymentMethod() === 'Cartão de crédito') { - } - +
+ @if (!valueSubmitted()) { + + } @else if (!paymentLocation() && !paymentMethod()) { + + } @else if (paymentLocation() === 'InLoco') { + + } @else if (paymentMethod() === 'Pix') { + + } @else if (paymentMethod() === 'Cartão de crédito') { + + } +
+ }
diff --git a/src/app/pages/top-up/top-up.component.ts b/src/app/pages/top-up/top-up.component.ts index f4d6b51..7ed0833 100644 --- a/src/app/pages/top-up/top-up.component.ts +++ b/src/app/pages/top-up/top-up.component.ts @@ -12,10 +12,14 @@ import { Router } from '@angular/router'; import { interval, takeUntil, timer } from 'rxjs'; +import { NgIcon, provideIcons } from '@ng-icons/core'; +import { lucideInfo } from '@ng-icons/lucide'; + import { HeaderComponent, HeaderType, } from '@rusbe/components/header/header.component'; +import { SpinnerComponent } from '@rusbe/components/spinner/spinner.component'; import { TopUpCalculatorComponent } from '@rusbe/components/top-up/calculator/top-up-calculator.component'; import { TopUpCreditCardComponent } from '@rusbe/components/top-up/credit-card/top-up-credit-card.component'; import { TopUpInLocoHelperComponent } from '@rusbe/components/top-up/in-loco-helper/top-up-in-loco-helper.component'; @@ -46,18 +50,34 @@ import { RusbeError } from '@rusbe/types/error-handling'; TopUpPixComponent, TopUpCreditCardComponent, TopUpInLocoHelperComponent, + SpinnerComponent, + NgIcon, ], templateUrl: './top-up.component.html', changeDetection: ChangeDetectionStrategy.OnPush, + viewProviders: [ + provideIcons({ + lucideInfo, + }), + ], }) export class TopUpComponent { readonly HEADER_TYPE = HeaderType.PageNameWithBackButton; readonly STAGE_MESSAGE = { - [TopUpStages.Calculator]: 'Quanto você quer adicionar?', - [TopUpStages.PaymentMethod]: 'Como você deseja adicionar créditos?', - [TopUpStages.InLocoHelper]: 'No guichê, mostre essas informações', - [TopUpStages.Pix]: '', - [TopUpStages.CreditCard]: '', + [TopUpStage.Calculator]: 'Quanto você quer adicionar?', + [TopUpStage.PaymentMethod]: 'Como você deseja adicionar créditos?', + [TopUpStage.InLocoHelper]: 'No guichê, mostre essas informações', + [TopUpStage.Pix]: '', + [TopUpStage.CreditCard]: '', + error: '', + }; + readonly ERROR_MESSAGES = { + [TopUpError.GeneralGoodsUnavailable]: + 'Infelizmente, o sistema da General Goods está fora do ar.', + [TopUpError.PixUnavailable]: + 'Ocorreu um erro ao tentar gerar o código pix.', + [TopUpError.Generic]: + 'Ocorreu um erro desconhecido. Por favor, tente novamente.', }; readonly FIFHTEEEN_MINUTES = 15 * 60 * 1000; @@ -72,7 +92,7 @@ export class TopUpComponent { paymentMethod = signal(null); paymentLocation = signal(null); pixTransactionData = signal(null); - pixErrorMessage = signal(''); + currentError = signal(null); remainingTimeString = signal(this.parseRemainingTime(0)); accountData = computed(() => { @@ -84,7 +104,6 @@ export class TopUpComponent { cpfNumber: accountData.cpfNumber, }; }); - currentStage = computed(() => { if (this.calculatorComponent()) { return 'calculator'; @@ -97,14 +116,10 @@ export class TopUpComponent { } else if (this.pixComponent()) { return 'pix'; } - return 'calculator'; + return 'error'; }); - topUpUnavailable = computed(() => { - const accountAuthState = this.authStateService.accountAuthState(); - - return accountAuthState === AccountAuthState.GeneralGoodsServiceUnavailable; - }); + accountAuthState = computed(() => this.authStateService.accountAuthState()); private readonly router = inject(Router); private readonly generalGoodsService = inject(GeneralGoodsService); @@ -114,6 +129,14 @@ export class TopUpComponent { effect(() => { if (this.pixComponent()) this.startPixTransaction(); }); + + effect(() => { + if (this.accountAuthState() === AccountAuthState.LoggedIn) { + this.currentError.set(null); + } else { + this.currentError.set(TopUpError.GeneralGoodsUnavailable); + } + }); } goToPreviousStage(): boolean { @@ -129,6 +152,8 @@ export class TopUpComponent { this.paymentLocation.set(null); this.paymentMethod.set(null); return true; + case 'error': + return false; default: return false; } @@ -138,6 +163,14 @@ export class TopUpComponent { this.router.navigate(['/']); } + retry(): void { + if (this.currentError() === TopUpError.GeneralGoodsUnavailable) { + this.authStateService.updateAccountAuthState(); + } else if (this.currentError() === TopUpError.PixUnavailable) { + this.currentError.set(null); + } + } + onPaymentMethodChange(paymentMethod: PaymentMethods) { this.paymentMethod.set(paymentMethod); } @@ -161,14 +194,10 @@ export class TopUpComponent { this.startPixTimer(); } catch (error) { if (error instanceof RusbeError) { - this.pixErrorMessage.set( - 'Ocorreu um erro ao tentar gerar o código pix.', - ); + this.currentError.set(TopUpError.PixUnavailable); return; } - this.pixErrorMessage.set( - 'Ocorreu um erro desconhecido. Por favor, tente novamente.', - ); + this.currentError.set(TopUpError.Generic); } } @@ -195,10 +224,16 @@ export class TopUpComponent { } } -export enum TopUpStages { +export enum TopUpStage { Calculator = 'calculator', PaymentMethod = 'payment-method', Pix = 'pix', CreditCard = 'credit-card', InLocoHelper = 'in-loco-helper', } + +enum TopUpError { + GeneralGoodsUnavailable = 'general-goods-unavailable', + PixUnavailable = 'pix-unavailable', + Generic = 'generic-error', +} diff --git a/src/styles.css b/src/styles.css index b0c931c..3c0deb8 100644 --- a/src/styles.css +++ b/src/styles.css @@ -224,7 +224,7 @@ } .grouping-card-strip-button { - @apply w-full touch-manipulation rounded-sm font-bold text-accent transition-all hover:bg-accent/10 focus:outline-none focus:ring focus:ring-accent focus:ring-offset-0 active:bg-accent/30 sm:gap-6; + @apply w-full touch-manipulation rounded-sm font-bold text-accent transition-all hover:bg-accent/10 focus:outline-none focus:ring focus:ring-accent focus:ring-offset-0 active:bg-accent/30 disabled:pointer-events-none sm:gap-6; } .grouping-card-strip-button-inner { From 89b4adc78700c3825e43cddf941e51cefa48fefa Mon Sep 17 00:00:00 2001 From: Guilherme Afonso Date: Wed, 5 Mar 2025 00:37:59 -0300 Subject: [PATCH 12/25] feat: ui improvements --- .../top-up-calculator.component.html | 9 +++-- .../calculator/top-up-calculator.component.ts | 8 +++++ .../top-up-in-loco-helper.component.html | 4 +-- .../top-up-in-loco-helper.component.ts | 5 ++- .../top-up/pix/top-up-pix.component.html | 33 +++++++++++++++---- .../top-up/pix/top-up-pix.component.ts | 14 +++++++- src/app/pages/top-up/top-up.component.html | 5 +-- src/app/pages/top-up/top-up.component.ts | 24 +++++++++++--- 8 files changed, 83 insertions(+), 19 deletions(-) diff --git a/src/app/components/top-up/calculator/top-up-calculator.component.html b/src/app/components/top-up/calculator/top-up-calculator.component.html index b1f09b0..a04f148 100644 --- a/src/app/components/top-up/calculator/top-up-calculator.component.html +++ b/src/app/components/top-up/calculator/top-up-calculator.component.html @@ -39,11 +39,16 @@
-
+
diff --git a/src/app/components/top-up/calculator/top-up-calculator.component.ts b/src/app/components/top-up/calculator/top-up-calculator.component.ts index 904e266..0fcd35f 100644 --- a/src/app/components/top-up/calculator/top-up-calculator.component.ts +++ b/src/app/components/top-up/calculator/top-up-calculator.component.ts @@ -16,6 +16,8 @@ import { Validators, } from '@angular/forms'; +import { NgIcon, provideIcons } from '@ng-icons/core'; +import { lucideMoveRight } from '@ng-icons/lucide'; import { NgxCurrencyDirective, NgxCurrencyInputMode } from 'ngx-currency'; import { MealBalanceBreakdownComponent } from '@rusbe/components/meal-balance-breakdown/meal-balance-breakdown.component'; @@ -30,12 +32,18 @@ import { WarningCardComponent } from '@rusbe/components/warning-card/warning-car NgxCurrencyDirective, ReactiveFormsModule, WarningCardComponent, + NgIcon, ], templateUrl: './top-up-calculator.component.html', changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'flex flex-grow flex-col items-start justify-start', }, + viewProviders: [ + provideIcons({ + lucideMoveRight, + }), + ], }) export class TopUpCalculatorComponent { submitted = output(); diff --git a/src/app/components/top-up/in-loco-helper/top-up-in-loco-helper.component.html b/src/app/components/top-up/in-loco-helper/top-up-in-loco-helper.component.html index 0bfe43b..33d12ac 100644 --- a/src/app/components/top-up/in-loco-helper/top-up-in-loco-helper.component.html +++ b/src/app/components/top-up/in-loco-helper/top-up-in-loco-helper.component.html @@ -16,7 +16,7 @@

Valor da Recarga

-
+

R$

{{ parsedValue() }} @@ -36,7 +36,7 @@ (click)="onBackClicked()" class="button-large button-split px-responsive bg-accent text-accent-contrast focus:ring-offset-background" > - Voltar + Finalizar (); parsedCpf = computed(() => this.parseCpfNumber()); - parsedValue = computed(() => parseFloat(this.topUpValue()).toFixed(2)); + parsedValue = computed(() => + BrlCurrency.fromNumber(parseFloat(this.topUpValue())), + ); onBackClicked(): void { this.backClicked.emit(); diff --git a/src/app/components/top-up/pix/top-up-pix.component.html b/src/app/components/top-up/pix/top-up-pix.component.html index e6675f1..c0a9ff8 100644 --- a/src/app/components/top-up/pix/top-up-pix.component.html +++ b/src/app/components/top-up/pix/top-up-pix.component.html @@ -1,9 +1,9 @@ @if (pixTransactionData()) { -

+
-
+

R$

{{ parsedTopUpValue() }} @@ -11,7 +11,7 @@

-

+

{{ pixTransactionData()?.qrCodeString }}

@@ -92,9 +92,17 @@

-

- Tempo restante para pagamento: {{ timeLeft() }} -

+
+ +

+ Você tem {{ timeLeft() }} para completar o + pagamento +

+
@@ -111,6 +119,19 @@ e recarga mostradas.

+
+ +
} @else {
(); pixQrCodeSource = computed(() => this.parseBase64ImageSource()); - parsedTopUpValue = computed(() => parseFloat(this.topUpValue()).toFixed(2)); + parsedTopUpValue = computed(() => + BrlCurrency.fromNumber(parseFloat(this.topUpValue())).toString(), + ); parsedCpf = computed(() => this.parseCpfNumber()); showPixQrCode = signal(false); @@ -68,6 +76,10 @@ export class TopUpPixComponent { this.copiedToClipboard.set(true); } + onFinish() { + this.finished.emit(); + } + private parseCurrentDate(): string { const dateString = this.CURRENT_DATE.toLocaleDateString('pt-BR', { day: '2-digit', diff --git a/src/app/pages/top-up/top-up.component.html b/src/app/pages/top-up/top-up.component.html index f48b9c9..c2c89ec 100644 --- a/src/app/pages/top-up/top-up.component.html +++ b/src/app/pages/top-up/top-up.component.html @@ -1,9 +1,9 @@ -
+
ADICIONAR CRÉDITOS @@ -86,6 +86,7 @@ [topUpValue]="value()" [timeLeft]="remainingTimeString()" [pixTransactionData]="pixTransactionData()" + (finished)="goToHome()" > } @else if (paymentMethod() === 'Cartão de crédito') { diff --git a/src/app/pages/top-up/top-up.component.ts b/src/app/pages/top-up/top-up.component.ts index 7ed0833..b24546a 100644 --- a/src/app/pages/top-up/top-up.component.ts +++ b/src/app/pages/top-up/top-up.component.ts @@ -2,6 +2,7 @@ import { CommonModule } from '@angular/common'; import { ChangeDetectionStrategy, Component, + OnDestroy, computed, effect, inject, @@ -10,7 +11,7 @@ import { } from '@angular/core'; import { Router } from '@angular/router'; -import { interval, takeUntil, timer } from 'rxjs'; +import { Subscription, interval, takeUntil, timer } from 'rxjs'; import { NgIcon, provideIcons } from '@ng-icons/core'; import { lucideInfo } from '@ng-icons/lucide'; @@ -61,8 +62,7 @@ import { RusbeError } from '@rusbe/types/error-handling'; }), ], }) -export class TopUpComponent { - readonly HEADER_TYPE = HeaderType.PageNameWithBackButton; +export class TopUpComponent implements OnDestroy { readonly STAGE_MESSAGE = { [TopUpStage.Calculator]: 'Quanto você quer adicionar?', [TopUpStage.PaymentMethod]: 'Como você deseja adicionar créditos?', @@ -95,6 +95,15 @@ export class TopUpComponent { currentError = signal(null); remainingTimeString = signal(this.parseRemainingTime(0)); + headerType = computed(() => { + if ( + this.currentStage() === 'calculator' || + this.currentStage() === 'payment-method' + ) { + return HeaderType.PageNameWithBackButton; + } + return HeaderType.PageNameWithCloseButton; + }); accountData = computed(() => { const accountData = this.authStateService.generalGoodsAccountData(); if (!accountData) return { fullName: '', cpfNumber: '' }; @@ -118,9 +127,10 @@ export class TopUpComponent { } return 'error'; }); - accountAuthState = computed(() => this.authStateService.accountAuthState()); + pixTimerSubscription?: Subscription; + private readonly router = inject(Router); private readonly generalGoodsService = inject(GeneralGoodsService); private readonly authStateService = inject(AuthStateService); @@ -139,6 +149,10 @@ export class TopUpComponent { }); } + ngOnDestroy(): void { + this.pixTimerSubscription?.unsubscribe(); + } + goToPreviousStage(): boolean { switch (this.currentStage()) { case 'calculator': @@ -206,7 +220,7 @@ export class TopUpComponent { const result = source.pipe(takeUntil(timer(this.FIFHTEEEN_MINUTES))); - result.subscribe({ + this.pixTimerSubscription = result.subscribe({ next: (timeSpent) => { this.remainingTimeString.set(this.parseRemainingTime(timeSpent)); }, From 1313f9782205eb20a319c45107aafe548d64cb3a Mon Sep 17 00:00:00 2001 From: Guilherme Afonso Date: Wed, 5 Mar 2025 02:08:09 -0300 Subject: [PATCH 13/25] feat: add balance preview on calculator --- .../calculator/top-up-calculator.component.html | 9 ++++++++- .../top-up/calculator/top-up-calculator.component.ts | 11 +++++++++++ src/app/pages/top-up/top-up.component.html | 1 + src/app/pages/top-up/top-up.component.ts | 6 ++++++ 4 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/app/components/top-up/calculator/top-up-calculator.component.html b/src/app/components/top-up/calculator/top-up-calculator.component.html index a04f148..f233357 100644 --- a/src/app/components/top-up/calculator/top-up-calculator.component.html +++ b/src/app/components/top-up/calculator/top-up-calculator.component.html @@ -39,7 +39,14 @@
-
+
+

+ Você tem R$ {{ currentBalance().value }} de saldo. Após a + transação, você ficará com R$ {{ newBalance() }}. +

Dados pessoais

-

- {{ parsedCpf() }} -

+ @if (parsedCpf()) { +

+ {{ parsedCpf() }} +

+ }

{{ name() }}

diff --git a/src/app/components/top-up/in-loco-helper/top-up-in-loco-helper.component.ts b/src/app/components/top-up/in-loco-helper/top-up-in-loco-helper.component.ts index af5c730..66b2284 100644 --- a/src/app/components/top-up/in-loco-helper/top-up-in-loco-helper.component.ts +++ b/src/app/components/top-up/in-loco-helper/top-up-in-loco-helper.component.ts @@ -47,7 +47,10 @@ export class TopUpInLocoHelperComponent { private parseCpfNumber(): string { const cpfNumber = this.cpf(); + if (!cpfNumber) return ''; + if (cpfNumber.length !== 11) return cpfNumber; + return cpfNumber.replace(/(\d{3})(\d{3})(\d{3})(\d{2})/, '$1.$2.$3-$4'); } } From 1cbbc14c5d7566fe874e70e8d736eeed2b3e5cbc Mon Sep 17 00:00:00 2001 From: Guilherme Afonso Date: Wed, 5 Mar 2025 22:28:51 -0300 Subject: [PATCH 19/25] fix: remove on push from top up page --- src/app/pages/top-up/top-up.component.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/app/pages/top-up/top-up.component.ts b/src/app/pages/top-up/top-up.component.ts index 4e8dd40..da47823 100644 --- a/src/app/pages/top-up/top-up.component.ts +++ b/src/app/pages/top-up/top-up.component.ts @@ -1,6 +1,5 @@ import { CommonModule } from '@angular/common'; import { - ChangeDetectionStrategy, Component, OnDestroy, computed, @@ -56,7 +55,6 @@ import { RusbeError } from '@rusbe/types/error-handling'; NgIcon, ], templateUrl: './top-up.component.html', - changeDetection: ChangeDetectionStrategy.OnPush, viewProviders: [ provideIcons({ lucideInfo, From 80545084772069c8353549ef078115721c6db567 Mon Sep 17 00:00:00 2001 From: Guilherme Afonso Date: Wed, 5 Mar 2025 22:29:36 -0300 Subject: [PATCH 20/25] refactor: title text case --- src/app/pages/top-up/top-up.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/pages/top-up/top-up.component.html b/src/app/pages/top-up/top-up.component.html index 836b9b3..af62ce9 100644 --- a/src/app/pages/top-up/top-up.component.html +++ b/src/app/pages/top-up/top-up.component.html @@ -5,7 +5,7 @@ [customAction]="goToPreviousStage.bind(this)" [type]="headerType()" > - ADICIONAR CRÉDITOS + Adicionar créditos
@let currentErrorValue = currentError(); From 2ff628498ce33710036a1b0df4037b59e9180373 Mon Sep 17 00:00:00 2001 From: Guilherme Afonso Date: Wed, 5 Mar 2025 22:32:18 -0300 Subject: [PATCH 21/25] fix: correct spelling of FIFTEEN_MINUTES constant --- src/app/pages/top-up/top-up.component.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/pages/top-up/top-up.component.ts b/src/app/pages/top-up/top-up.component.ts index da47823..b0d91e5 100644 --- a/src/app/pages/top-up/top-up.component.ts +++ b/src/app/pages/top-up/top-up.component.ts @@ -78,7 +78,7 @@ export class TopUpComponent implements OnDestroy { [TopUpError.Generic]: 'Ocorreu um erro desconhecido. Por favor, tente novamente.', }; - readonly FIFHTEEEN_MINUTES = 15 * 60 * 1000; + readonly FIFTEEN_MINUTES = 15 * 60 * 1000; calculatorComponent = viewChild(TopUpCalculatorComponent); paymentMethodComponent = viewChild(TopUpPaymentMethodComponent); @@ -222,7 +222,7 @@ export class TopUpComponent implements OnDestroy { private startPixTimer() { const source = interval(1000); - const result = source.pipe(takeUntil(timer(this.FIFHTEEEN_MINUTES))); + const result = source.pipe(takeUntil(timer(this.FIFTEEN_MINUTES))); this.pixTimerSubscription = result.subscribe({ next: (timeSpent) => { @@ -235,7 +235,7 @@ export class TopUpComponent implements OnDestroy { } private parseRemainingTime(timeSpent: number): string { - const remainingTime = this.FIFHTEEEN_MINUTES - timeSpent * 1000; + const remainingTime = this.FIFTEEN_MINUTES - timeSpent * 1000; const minutes = Math.floor(remainingTime / 60000); const seconds = Math.floor((remainingTime % 60000) / 1000); return `${minutes}:${seconds < 10 ? '0' : ''}${seconds}`; From 6e34d275799d6137739013fc8685e4d973a7b870 Mon Sep 17 00:00:00 2001 From: Guilherme Afonso <18534480+Guilhermeasper@users.noreply.github.com> Date: Wed, 5 Mar 2025 22:34:33 -0300 Subject: [PATCH 22/25] fix: use enum instead of hardcoded values Co-authored-by: Erick Almeida --- src/app/pages/top-up/top-up.component.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/pages/top-up/top-up.component.ts b/src/app/pages/top-up/top-up.component.ts index b0d91e5..2285a76 100644 --- a/src/app/pages/top-up/top-up.component.ts +++ b/src/app/pages/top-up/top-up.component.ts @@ -96,8 +96,8 @@ export class TopUpComponent implements OnDestroy { headerType = computed(() => { if ( - this.currentStage() === 'calculator' || - this.currentStage() === 'payment-method' + this.currentStage() === TopUpStage.Calculator || + this.currentStage() === TopUpStage.PaymentMethod ) { return HeaderType.PageNameWithBackButton; } From 19918fab69ed6ad93834a0d5d4c0868d3512f222 Mon Sep 17 00:00:00 2001 From: Guilherme Afonso <18534480+Guilhermeasper@users.noreply.github.com> Date: Wed, 5 Mar 2025 22:34:50 -0300 Subject: [PATCH 23/25] refactor: pix spelling Co-authored-by: Erick Almeida --- src/app/pages/top-up/top-up.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/pages/top-up/top-up.component.ts b/src/app/pages/top-up/top-up.component.ts index 2285a76..fa8aa19 100644 --- a/src/app/pages/top-up/top-up.component.ts +++ b/src/app/pages/top-up/top-up.component.ts @@ -74,7 +74,7 @@ export class TopUpComponent implements OnDestroy { [TopUpError.GeneralGoodsUnavailable]: 'Infelizmente, o sistema da General Goods está fora do ar.', [TopUpError.PixUnavailable]: - 'Ocorreu um erro ao tentar gerar o código pix.', + 'Ocorreu um erro ao tentar gerar o código Pix.', [TopUpError.Generic]: 'Ocorreu um erro desconhecido. Por favor, tente novamente.', }; From 0dd088ca8cf211bab2a725ebb7cd3cce5dd09927 Mon Sep 17 00:00:00 2001 From: Guilherme Afonso Date: Wed, 5 Mar 2025 22:43:28 -0300 Subject: [PATCH 24/25] fix: check cpf length before parsing --- src/app/components/top-up/pix/top-up-pix.component.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/app/components/top-up/pix/top-up-pix.component.ts b/src/app/components/top-up/pix/top-up-pix.component.ts index bd57bbf..67e5271 100644 --- a/src/app/components/top-up/pix/top-up-pix.component.ts +++ b/src/app/components/top-up/pix/top-up-pix.component.ts @@ -101,7 +101,10 @@ export class TopUpPixComponent { private parseCpfNumber(): string { const cpfNumber = this.cpf(); + if (!cpfNumber) return ''; + if (cpfNumber.length !== 11) return cpfNumber; + return cpfNumber.replace(/(\d{3})(\d{3})(\d{3})(\d{2})/, '•••.$2.$3-••'); } } From 225a023c7e09290a44ddbf52771836c5252d1918 Mon Sep 17 00:00:00 2001 From: Guilherme Afonso Date: Wed, 5 Mar 2025 23:30:04 -0300 Subject: [PATCH 25/25] fix: enhance balance preview --- .../calculator/top-up-calculator.component.html | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/app/components/top-up/calculator/top-up-calculator.component.html b/src/app/components/top-up/calculator/top-up-calculator.component.html index f233357..7d21b36 100644 --- a/src/app/components/top-up/calculator/top-up-calculator.component.html +++ b/src/app/components/top-up/calculator/top-up-calculator.component.html @@ -42,11 +42,17 @@
-

- Você tem R$ {{ currentBalance().value }} de saldo. Após a - transação, você ficará com R$ {{ newBalance() }}. -

+ @if (currentBalance().value.toString() !== newBalance().toString()) { +

+ Você tem R$ {{ currentBalance().value }} de saldo. Após + a transação, você ficará com R$ {{ newBalance() }}. +

+ } @else { +

+ Você tem R$ {{ currentBalance().value }} de saldo. +

+ }