Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implementation - connect cards input UI #3388

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
<div class="connect-card__body">
<div>
<div class="connect-card__heading">Connect corporate card</div>
<div class="connect-card__sub-heading">
This will help you bring your card transactions into Fyle as expenses instantly.
</div>

<div class="connect-card__input-label">
<span>Corporate card</span>
</div>

<div
class="connect-card__input-inner-container"
[ngClass]="{ 'connect-card__input-inner-container--error': cardForm.touched && cardForm.invalid }"
>
<input
class="smartlook-show connect-card__card-number-input pl-0"
inputmode="numeric"
[formControl]="cardForm"
mask="0000 0000 0000 0000"
data-testid="card-number-input"
appAutofocus
[timeout]="500"
required
placeholder="Enter corporate card number"
/>

<ion-icon
*ngIf="!cardType || cardType === cardNetworkTypes.OTHERS"
src="../../../../assets/svg/card.svg"
class="connect-card__input-default-icon"
data-testid="default-icon"
></ion-icon>

<img
*ngIf="cardType === cardNetworkTypes.VISA"
src="../../../../assets/images/visa-logo.png"
class="connect-card__input-visa-icon"
data-testid="visa-icon"
/>

<img
*ngIf="cardType === cardNetworkTypes.MASTERCARD"
src="../../../../assets/images/mastercard-logo.png"
class="connect-card__input-mastercard-icon"
data-testid="mastercard-icon"
/>
</div>

<div class="connect-card__input-error-space"></div>

<div *ngIf="cardForm.touched && cardForm.invalid" class="connect-card__input-errors" data-testid="error-message">
<span *ngIf="cardForm.errors.invalidCardNumber">Please enter a valid card number.</span>

<ng-container *ngIf="cardForm.errors.invalidCardNetwork">
<span *ngIf="isVisaRTFEnabled && isMastercardRTFEnabled; else visaOnlyOrg"
>Enter a valid Visa or Mastercard number. If you have other cards, please contact your admin.</span
>

<ng-template #visaOnlyOrg>
<!-- Check if only visa is enabled -->
<span *ngIf="cardForm.errors.invalidCardNetwork && isVisaRTFEnabled; else mastercardOnlyOrg"
>Enter a valid Visa number. If you have other cards, please contact your admin.</span
>
</ng-template>

<ng-template #mastercardOnlyOrg>
<!-- Check if only mastercard is enabled -->
<span *ngIf="cardForm.errors.invalidCardNetwork && isMastercardRTFEnabled"
>Enter a valid Mastercard number. If you have other cards, please contact your admin.</span
>
</ng-template>
</ng-container>

<span *ngIf="cardForm.errors.enrollmentError">
{{ enrollmentFailureMessage }}
</span>
</div>
</div>
<div class="connect-card__primary-cta-container">
<ion-button
class="btn-primary connect-card__primary-cta"
fill="clear"
aria-label="Navigate back to sign in page"
role="button"
>
Continue
</ion-button>
</div>
Comment on lines +81 to +89
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Disable the “Continue” button if invalid, perhaps?
Currently, it’s always clickable. Disabling it when cardForm.invalid would provide a more guided UX.

<ion-button
  class="btn-primary connect-card__primary-cta"
  fill="clear"
  aria-label="Navigate back to sign in page"
  role="button"
+ [disabled]="cardForm.invalid"
>
  Continue
</ion-button>

Committable suggestion skipped: line range outside the PR's diff.

</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
@import '../../../../theme/colors.scss';

.connect-card {
position: relative;
height: 100%;

&__body {
display: flex;
flex-direction: column;
justify-content: space-between;
height: 100%;
}
Comment on lines +7 to +12
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

Double .connect-card__body definitions, la?
Lines 7–12 and 54–58 both define .connect-card__body. Merging these blocks would keep the CSS crisp and reduce confusion.

.connect-card__body {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  height: 100%;

-}

-&__body {
-  display: flex;
-  flex-direction: column;
+  // additional nested rules here if needed
}

Also applies to: 54-58


&__primary-cta-container {
display: flex;
flex-direction: row;
justify-content: flex-end;
}

&__heading {
color: $black;
font-size: 20px;
font-weight: 500;
line-height: normal;
margin-bottom: 8px;
margin-top: 32px;
}

&__sub-heading {
color: $dark-grey;
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 1.28;
margin-bottom: 32px;
}

&__primary-cta {
width: 108px;
align-self: flex-end;
}

&__toolbar-title {
font-size: 20px;
font-weight: 500;
color: $black;
line-height: normal;
}

&__toolbar-close-btn {
position: absolute;
}

&__body {
display: flex;
flex-direction: column;
}

&__content {
--padding-start: 16px;
--padding-end: 16px;
--padding-top: 16px;

max-height: 50vh;
}

&__input-container {
padding: 16px 16px 8px 16px;
border-radius: 8px;
border: 1px solid $grey;

&:focus-within:not(&--error) {
border: 1px solid $black-light;
}
}

&__input-label {
font-size: 12px;
color: $black-light;
margin-bottom: 6px;
}

&__input-inner-container {
display: flex;
align-items: center;
border-bottom: 1px solid $grey;
padding-bottom: 6px;
margin-bottom: 2px;

&--error {
border-bottom: 1px solid $red;
}

&:focus-within:not(&--error) {
border-bottom: 1px solid $black-light;
}
}

&__card-number-input {
border: 0;
color: $blue-black;
width: 100%;
}

&__input-default-icon {
width: 20px;
height: 20px;
color: $grey-light;
}

&__input-visa-icon,
&__input-mastercard-icon {
width: 38px;
height: 22px;
}

&__input-error-space {
width: 0px;
height: 16px;
float: right;
}

&__input-errors {
color: $red;
font-size: 12px;
line-height: 1.3;

& > :not(:first-child) {
// Only show one error message at a time
display: none;
}
}

&__view-tnc-btn {
width: 100%;
background: $pink-gradient;
background-clip: text;
color: transparent;
padding: 12px 0;
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
font-size: 14px;
}

&__view-tnc-btn-icon {
width: 18px;
height: 18px;
color: $brand-primary;
}

&__tnc {
display: flex;
flex-direction: column;
gap: 24px;
padding-top: 8px;
font-size: 14px;
}

&__tnc-heading {
font-weight: 500;
color: $black;
}

&__tnc-list {
margin: 0;
padding-left: 20px;
display: flex;
flex-direction: column;
gap: 16px;
}

&__tnc-link {
color: $blue-4;
text-decoration: none;
}

&__tnc-link-icon {
width: 16px;
height: 16px;
vertical-align: text-bottom;
}

&__footer-toolbar {
padding: 16px;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { AbstractControl, FormControl, ValidationErrors } from '@angular/forms';
import { CardNetworkType } from 'src/app/core/enums/card-network-type';
import { OrgSettings } from 'src/app/core/models/org-settings.model';
import { CorporateCreditCardExpenseService } from 'src/app/core/services/corporate-credit-card-expense.service';
import { RealTimeFeedService } from 'src/app/core/services/real-time-feed.service';

@Component({
selector: 'app-spender-onboarding-connect-card-step',
templateUrl: './spender-onboarding-connect-card-step.component.html',
styleUrls: ['./spender-onboarding-connect-card-step.component.scss'],
})
export class SpenderOnboardingConnectCardStepComponent {
@Input() readOnly?: boolean = false;

@Input() orgSettings: OrgSettings;

@Output() isStepCompleted: EventEmitter<boolean> = new EventEmitter<boolean>();

cardForm: FormControl;

isVisaRTFEnabled = false;

isMastercardRTFEnabled = false;

cardType = CardNetworkType;

constructor(
private corporateCreditCardExpensesService: CorporateCreditCardExpenseService,
private realTimeFeedService: RealTimeFeedService
) {}

ionViewWillEnter(): void {
this.cardForm = new FormControl('', [this.cardNumberValidator.bind(this), this.cardNetworkValidator.bind(this)]);
}

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 cardType = this.realTimeFeedService.getCardTypeFromNumber(cardNumber);

if (cardType === CardNetworkType.VISA || cardType === CardNetworkType.MASTERCARD) {
return isValid && cardNumber.length === 16 ? null : { invalidCardNumber: true };
}

return isValid ? null : { invalidCardNumber: true };
}

private cardNetworkValidator(control: AbstractControl): ValidationErrors {
const cardNumber = control.value as string;
const cardType = this.realTimeFeedService.getCardTypeFromNumber(cardNumber);

if (
(!this.isVisaRTFEnabled && cardType === CardNetworkType.VISA) ||
(!this.isMastercardRTFEnabled && cardType === CardNetworkType.MASTERCARD)
) {
return { invalidCardNetwork: true };
}

return null;
}
Comment on lines +52 to +64
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick (assertive)

One small check, thambi – what about other networks?
If your usage only supports Visa/Mastercard, that’s fine. If you might expand in the future (like adding Amex?), consider a clarifying note or a future-proof architecture.

}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ $toolbar-border: #ababab6b;
padding: 20px 16px;
}

&__component-container {
height: 75%;
}

&__step-tracker {
padding: 24px;
border-radius: 32px 32px 0px 0px;
Expand Down
Loading