-
Notifications
You must be signed in to change notification settings - Fork 15
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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> | ||
</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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick (assertive) Double .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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick (assertive) One small check, thambi – what about other networks? |
||
} |
There was a problem hiding this comment.
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.