diff --git a/src/app/core/models/popover-cards-list.model.ts b/src/app/core/models/popover-cards-list.model.ts new file mode 100644 index 0000000000..a3828bb505 --- /dev/null +++ b/src/app/core/models/popover-cards-list.model.ts @@ -0,0 +1,4 @@ +export interface PopoverCardsList { + successfulCards: string[]; + failedCards: string[]; +} diff --git a/src/app/fyle/spender-onboarding/models/onboarding-step.enum.ts b/src/app/fyle/spender-onboarding/models/onboarding-step.enum.ts index 45b3a27da1..de9f4be01f 100644 --- a/src/app/fyle/spender-onboarding/models/onboarding-step.enum.ts +++ b/src/app/fyle/spender-onboarding/models/onboarding-step.enum.ts @@ -1,4 +1,4 @@ export enum OnboardingStep { - CONNECT_CARD, - OPT_IN, + CONNECT_CARD = 'CONNECT_CARD', + OPT_IN = 'OPT_IN', } diff --git a/src/app/fyle/spender-onboarding/spender-onboarding-connect-card-step/spender-onboarding-connect-card-step.component.html b/src/app/fyle/spender-onboarding/spender-onboarding-connect-card-step/spender-onboarding-connect-card-step.component.html index 8c31a5e04b..0ef099030a 100644 --- a/src/app/fyle/spender-onboarding/spender-onboarding-connect-card-step/spender-onboarding-connect-card-step.component.html +++ b/src/app/fyle/spender-onboarding/spender-onboarding-connect-card-step/spender-onboarding-connect-card-step.component.html @@ -1,12 +1,11 @@ -
+
Connect corporate card
This will help you bring your card transactions into Fyle as expenses instantly.
- -
-
+ +
Corporate card @@ -14,22 +13,26 @@
- +
+ -
- {{ card?.card_number || '' }} +
+ {{ cardValuesMap[card.id].last_four || '' }} +
- Please enter a valid card number. - + Enter a valid Visa or Mastercard number. If you have other cards, please contact your admin. @@ -74,7 +77,7 @@ Enter a valid Visa number. If you have other cards, please contact your admin. - Enter a valid Mastercard number. If you have other cards, please contact your admin. @@ -91,14 +94,90 @@
-
+
+
+ Corporate card +
+ +
+ + + + + + + +
+ +
+ +
+ Please enter a valid card number. + + + Enter a valid Visa or Mastercard number. If you have other cards, please contact your admin. + + + + Enter a valid Visa number. If you have other cards, please contact your admin. + + + + + Enter a valid Mastercard number. If you have other cards, please contact your admin. + + +
+
diff --git a/src/app/fyle/spender-onboarding/spender-onboarding-connect-card-step/spender-onboarding-connect-card-step.component.scss b/src/app/fyle/spender-onboarding/spender-onboarding-connect-card-step/spender-onboarding-connect-card-step.component.scss index 360329bd83..b20f990794 100644 --- a/src/app/fyle/spender-onboarding/spender-onboarding-connect-card-step/spender-onboarding-connect-card-step.component.scss +++ b/src/app/fyle/spender-onboarding/spender-onboarding-connect-card-step/spender-onboarding-connect-card-step.component.scss @@ -18,13 +18,36 @@ } &__card-number-input { - width: fit-content !important; margin-right: 24px; + font-size: 18px; + font-weight: 500; &::placeholder { + font-size: 14px; + font-weight: 400; word-spacing: 24px; } } + &__card-number-input-singular { + border: 0; + font-size: 18px; + font-weight: 500; + &::placeholder { + font-size: 14px; + font-weight: 400; + } + } + + &__card-number-input-container { + display: flex; + justify-content: space-between; + } + + &__card-last-four { + font-size: 18px; + font-weight: 500; + } + &__heading { color: $black; font-size: 20px; @@ -91,6 +114,7 @@ &__input-inner-container { display: flex; align-items: center; + justify-content: space-between; border-bottom: 1px solid $grey; padding-bottom: 6px; margin-bottom: 2px; diff --git a/src/app/fyle/spender-onboarding/spender-onboarding-connect-card-step/spender-onboarding-connect-card-step.component.ts b/src/app/fyle/spender-onboarding/spender-onboarding-connect-card-step/spender-onboarding-connect-card-step.component.ts index 60b64498a9..dc8d799401 100644 --- a/src/app/fyle/spender-onboarding/spender-onboarding-connect-card-step/spender-onboarding-connect-card-step.component.ts +++ b/src/app/fyle/spender-onboarding/spender-onboarding-connect-card-step/spender-onboarding-connect-card-step.component.ts @@ -1,4 +1,13 @@ -import { Component, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core'; +import { + ChangeDetectorRef, + Component, + EventEmitter, + Input, + OnChanges, + OnInit, + Output, + SimpleChanges, +} from '@angular/core'; import { AbstractControl, FormArray, @@ -9,9 +18,8 @@ import { Validators, } from '@angular/forms'; import { PopoverController } from '@ionic/angular'; -import { catchError, concatMap, finalize, from, map, noop, of, switchMap, tap } from 'rxjs'; +import { catchError, concatMap, from, map, noop, of, switchMap, tap } from 'rxjs'; import { CardNetworkType } from 'src/app/core/enums/card-network-type'; -import { statementUploadedCard, visaRTFCard } from 'src/app/core/mock-data/platform-corporate-card.data'; import { OrgSettings } from 'src/app/core/models/org-settings.model'; import { OverlayResponse } from 'src/app/core/models/overlay-response.modal'; import { PlatformCorporateCard } from 'src/app/core/models/platform/platform-corporate-card.model'; @@ -25,7 +33,6 @@ import { PopupAlertComponent } from 'src/app/shared/components/popup-alert/popup templateUrl: './spender-onboarding-connect-card-step.component.html', styleUrls: ['./spender-onboarding-connect-card-step.component.scss'], }) - export class SpenderOnboardingConnectCardStepComponent implements OnInit, OnChanges { @Input() readOnly?: boolean = false; @@ -41,12 +48,19 @@ export class SpenderOnboardingConnectCardStepComponent implements OnInit, OnChan cardType = CardNetworkType; - enrollableCards: PlatformCorporateCard[]; + enrollableCards: PlatformCorporateCard[] = []; - cardValuesMap: Record = {}; + cardValuesMap: Record = {}; rtfCardType: CardNetworkType; + cardsLoading = true; + + singleEnrollableCardDetails: { card_type: string; card_number: string } = { + card_type: '', + card_number: '', + }; + cardsList: PopoverCardsList = { successfulCards: [], failedCards: [], @@ -66,15 +80,17 @@ export class SpenderOnboardingConnectCardStepComponent implements OnInit, OnChan from(cards) .pipe( concatMap((card) => - this.realTimeFeedService.enroll(card.card_number, card.id).pipe( - map(() => { - this.cardsList.successfulCards.push(`**** ${card.card_number.slice(-4)}`); - }), - catchError(() => { - this.cardsList.failedCards.push(`**** ${card.card_number.slice(-4)}`); - return of(null); - }) - ) + this.realTimeFeedService + .enroll(this.fg.controls[`card_number_${card.id}`].value + this.cardValuesMap[card.id].last_four, card.id) + .pipe( + map(() => { + this.cardsList.successfulCards.push(`**** ${card.card_number.slice(-4)}`); + }), + catchError(() => { + this.cardsList.failedCards.push(`**** ${card.card_number.slice(-4)}`); + return of(null); + }) + ) ) ) .subscribe(() => { @@ -89,13 +105,13 @@ export class SpenderOnboardingConnectCardStepComponent implements OnInit, OnChan generateMessage(): string { if (this.cardsList.successfulCards.length > 0) { return 'We ran into an issue while processing your request. You can cancel and retry connecting the failed card or proceed to the next step.'; - } else if (this.cardsList.successfulCards.length > 0) { + } else if (this.cardsList.failedCards.length > 0) { return ` We ran into an issue while processing your request for the card ${this.cardsList.failedCards[0]}. You can cancel and retry connecting the failed card or proceed to the next step.`; } else { return ` - We ran into an issue while processing your request for the card ${this.cardsList.failedCards + We ran into an issue while processing your request for the cards ${this.cardsList.failedCards .slice(this.cardsList.failedCards.length - 1) .join(', ')} and ${this.cardsList.failedCards.slice(-1)}. You can cancel and retry connecting the failed card or proceed to the next step.`; @@ -105,7 +121,7 @@ export class SpenderOnboardingConnectCardStepComponent implements OnInit, OnChan showErrorPopover(): void { const errorPopover = this.popoverController.create({ componentProps: { - title: 'Status summary', + title: this.cardsList.successfulCards.length > 0 ? 'Status summary' : 'Failed connecting', message: this.generateMessage(), primaryCta: { text: 'Proceed anyway', @@ -141,60 +157,75 @@ export class SpenderOnboardingConnectCardStepComponent implements OnInit, OnChan } } - ngOnInit(): void { - this.fg = this.fb.group({}); + setupForm(): void { + this.cardsLoading = true; this.corporateCreditCardExpensesService .getCorporateCards() .pipe( map((corporateCards) => { - // Filter enrollable cards this.enrollableCards = corporateCards.filter((card) => card.data_feed_source === 'STATEMENT_UPLOAD'); - // Add form controls for each enrollable card - this.enrollableCards.forEach((card, index) => { - const controlName = `card_number_${index}`; - this.cardValuesMap[card.id] = { - card_number: card.card_number, + if (this.enrollableCards.length > 0) { + this.enrollableCards.forEach((card) => { + const controlName = `card_number_${card.id}`; + this.cardValuesMap[card.id] = { + last_four: card.card_number.slice(-4), + card_type: CardNetworkType.OTHERS, + }; + this.fg.addControl( + controlName, + new FormControl('', [ + Validators.required, + Validators.maxLength(12), + this.cardNumberValidator.bind(this), + this.cardNetworkValidator.bind(this), + ]) + ); + }); + } else { + this.singleEnrollableCardDetails = { + card_number: null, card_type: CardNetworkType.OTHERS, }; this.fg.addControl( - controlName, - this.fb.control('', [ + 'card_number', + new FormControl('', [ Validators.required, - Validators.maxLength(12), + Validators.maxLength(16), this.cardNumberValidator.bind(this), this.cardNetworkValidator.bind(this), ]) ); - }); + } + this.cardsLoading = false; }) ) .subscribe(); } - onCardNumberUpdate(card: PlatformCorporateCard, inputControlName: string): void { - this.formatCardNumber(this.fg.controls[inputControlName]); - this.cardValuesMap[card.id].card_type = this.realTimeFeedService.getCardTypeFromNumber( - this.cardValuesMap[card.id].card_number - ); + ngOnInit(): void { + this.fg = this.fb.group({}); + this.setupForm(); } - formatCardNumber(input: AbstractControl): void { - // Remove all non-numeric characters - let value = (input.value as string).replace(/\D/g, ''); - - // Format the value in groups of 4 - value = value.replace(/(\d{4})(?=\d)/g, '$1 '); - - // Set the formatted value back to the input - input.setValue(value); + onCardNumberUpdate(card?: PlatformCorporateCard): void { + if (this.enrollableCards.length > 0) { + this.cardValuesMap[card.id].card_type = this.realTimeFeedService.getCardTypeFromNumber( + this.fg.controls[`card_number_${card.id}`].value as string + ); + } else { + this.singleEnrollableCardDetails.card_type = this.realTimeFeedService.getCardTypeFromNumber( + this.fg.controls.card_number.value as string + ); + } + } private cardNumberValidator(control: AbstractControl): ValidationErrors { // Reactive forms are not strongly typed in Angular 13, so we need to cast the value to string // TODO (Angular 14 >): Remove the type casting and directly use string type for the form control const cardNumber = control.value as string; - const isValid = this.realTimeFeedService.isCardNumberValid(cardNumber); + const isValid = this.realTimeFeedService.isCardNumberValid(cardNumber.replace(/ /g, '')); const cardType = this.realTimeFeedService.getCardTypeFromNumber(cardNumber); if (cardType === CardNetworkType.VISA || cardType === CardNetworkType.MASTERCARD) { diff --git a/src/app/fyle/spender-onboarding/spender-onboarding.module.ts b/src/app/fyle/spender-onboarding/spender-onboarding.module.ts index f5a3827051..888dfe0ec8 100644 --- a/src/app/fyle/spender-onboarding/spender-onboarding.module.ts +++ b/src/app/fyle/spender-onboarding/spender-onboarding.module.ts @@ -9,6 +9,7 @@ import { MatButtonModule } from '@angular/material/button'; import { SpenderOnboardingConnectCardStepComponent } from './spender-onboarding-connect-card-step/spender-onboarding-connect-card-step.component'; import { SpenderOnboardingOptInStepComponent } from './spender-onboarding-opt-in-step/spender-onboarding-opt-in-step.component'; import { NgOtpInputModule } from 'ng-otp-input'; +import { NgxMaskModule } from 'ngx-mask'; @NgModule({ imports: [ @@ -21,6 +22,9 @@ import { NgOtpInputModule } from 'ng-otp-input'; FormsModule, ReactiveFormsModule, NgOtpInputModule, + NgxMaskModule.forRoot({ + validation: false, + }), ], declarations: [SpenderOnboardingPage, SpenderOnboardingConnectCardStepComponent, SpenderOnboardingOptInStepComponent], }) diff --git a/src/app/fyle/spender-onboarding/spender-onboarding.page.html b/src/app/fyle/spender-onboarding/spender-onboarding.page.html index 62b6f32359..5807b7e12d 100644 --- a/src/app/fyle/spender-onboarding/spender-onboarding.page.html +++ b/src/app/fyle/spender-onboarding/spender-onboarding.page.html @@ -21,9 +21,10 @@ class="spender-onboarding__progress-bar spender-onboarding__progress-bar-right" [src]="'/assets/svg/progress-bar.svg'" slot="icon-only" + [ngClass]="{'spender-onboarding__step-next': currentStep === 'CONNECT_CARD', 'spender-onboarding__step-hide': onlyOptInEnabled}" >
-
Skip
+
Skip
{ this.eou = eou; this.userFullName = eou.us.full_name; + this.orgSettings = orgSettings; const isRtfEnabled = orgSettings.visa_enrollment_settings.enabled && orgSettings.mastercard_enrollment_settings.enabled; const isAmexFeedEnabled = orgSettings.amex_feed_enrollment_settings.enabled; const rtfCards = corporateCards.filter((card) => card.is_visa_enrolled || card.is_mastercard_enrolled); if (isAmexFeedEnabled && !isRtfEnabled) { this.currentStep = OnboardingStep.OPT_IN; + this.onlyOptInEnabled = true; } else if (isRtfEnabled) { // If Connect Card was skipped earlier or Cards are already enrolled, then go to OPT_IN step if ( @@ -63,6 +70,7 @@ export class SpenderOnboardingPage { rtfCards.length > 0 ) { this.currentStep = OnboardingStep.OPT_IN; + this.onlyOptInEnabled = true; } else { this.currentStep = OnboardingStep.CONNECT_CARD; } diff --git a/src/app/shared/components/popup-alert/popup-alert.component.scss b/src/app/shared/components/popup-alert/popup-alert.component.scss index 7d64099f53..fe428ae9a3 100644 --- a/src/app/shared/components/popup-alert/popup-alert.component.scss +++ b/src/app/shared/components/popup-alert/popup-alert.component.scss @@ -71,22 +71,6 @@ $small-screen: 700px; fill: $green; } - &--success-tick-container { - border-radius: 4px; - background: $success-bg; - width: 24px; - height: 24px; - display: flex; - align-items: center; - justify-content: center; - } - - &--success-tick { - height: 14px; - width: 14px; - fill: $green; - } - &--error-tick-container { border-radius: 4px; background: $pale-pink; diff --git a/src/app/shared/components/popup-alert/popup-alert.component.ts b/src/app/shared/components/popup-alert/popup-alert.component.ts index 38329ef83c..7b5ada53f9 100644 --- a/src/app/shared/components/popup-alert/popup-alert.component.ts +++ b/src/app/shared/components/popup-alert/popup-alert.component.ts @@ -1,6 +1,5 @@ import { Component, Input } from '@angular/core'; import { PopoverController } from '@ionic/angular'; -import { CardNetworkType } from 'src/app/core/enums/card-network-type'; import { PopoverCardsList } from 'src/app/core/models/popover-cards-list.model'; @Component({ selector: 'app-popup-alert',