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 for connect cards part 2 #3389

Merged
Show file tree
Hide file tree
Changes from 7 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
14 changes: 14 additions & 0 deletions src/app/core/mock-data/onboarding-status.data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import deepFreeze from 'deep-freeze-strict';
import { OnboardingStatus } from '../models/onboarding-status.model';
import { OnboardingState } from '../models/onboarding-state.enum';

export const onboardingStatusData: OnboardingStatus = deepFreeze({
user_id: 'us1ymEVgUKqb',
org_id: 'orOTDe765hQp',
step_connect_cards_is_configured: false,
step_connect_cards_is_skipped: false,
step_sms_opt_in_is_configured: false,
step_sms_opt_in_is_skipped: false,
step_show_welcome_modal_is_complete: false,
state: OnboardingState.YET_TO_START,
});
144 changes: 144 additions & 0 deletions src/app/core/services/spender-onboarding.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import { TestBed } from '@angular/core/testing';
import { SpenderOnboardingService } from './spender-onboarding.service';
import { SpenderPlatformV1ApiService } from './spender-platform-v1-api.service';
import { OnboardingStepStatus } from '../models/onboarding-step-status.model';
import { of } from 'rxjs';
import { onboardingStatusData } from '../mock-data/onboarding-status.data';
import { OnboardingWelcomeStepStatus } from '../models/onboarding-welcome-step-status.model';

describe('SpenderOnboardingService', () => {
let spenderOnboardingService: SpenderOnboardingService;
let spenderPlatformV1ApiService: jasmine.SpyObj<SpenderPlatformV1ApiService>;

beforeEach(() => {
const spenderPlatformV1ApiServiceSpy = jasmine.createSpyObj('SpenderPlatformV1ApiService', ['get', 'post']);
TestBed.configureTestingModule({
providers: [
SpenderOnboardingService,
[
{
provide: SpenderPlatformV1ApiService,
useValue: spenderPlatformV1ApiServiceSpy,
},
],
],
});
spenderOnboardingService = TestBed.inject(SpenderOnboardingService);
spenderPlatformV1ApiService = TestBed.inject(
SpenderPlatformV1ApiService
) as jasmine.SpyObj<SpenderPlatformV1ApiService>;
});

it('getOnboardingStatus(): should get onboarding status', (done) => {
const onboardingResponse = onboardingStatusData;
spenderPlatformV1ApiService.get.and.returnValue(of({ data: onboardingResponse }));

spenderOnboardingService.getOnboardingStatus().subscribe((res) => {
expect(res).toEqual(onboardingResponse);
expect(spenderPlatformV1ApiService.get).toHaveBeenCalledOnceWith('/onboarding');
done();
});
});

it('processConnnectCardsStep(): should process connect card step', (done) => {
const onboardingRequestResponse: OnboardingStepStatus = {
is_configured: true,
is_skipped: false,
};
spenderPlatformV1ApiService.post.and.returnValue(of({ data: onboardingRequestResponse }));

spenderOnboardingService.processConnectCardsStep(onboardingRequestResponse).subscribe((res) => {
expect(res).toEqual(onboardingRequestResponse);
expect(spenderPlatformV1ApiService.post).toHaveBeenCalledOnceWith('/onboarding/process_step_connect_cards', {
data: onboardingRequestResponse,
});
done();
});
});

it('processSmsOptInStep(): should process opt in step', (done) => {
const onboardingRequestResponse: OnboardingStepStatus = {
is_configured: true,
is_skipped: false,
};
spenderPlatformV1ApiService.post.and.returnValue(of({ data: onboardingRequestResponse }));

spenderOnboardingService.processSmsOptInStep(onboardingRequestResponse).subscribe((res) => {
expect(res).toEqual(onboardingRequestResponse);
expect(spenderPlatformV1ApiService.post).toHaveBeenCalledOnceWith('/onboarding/process_step_sms_opt_in', {
data: onboardingRequestResponse,
});
done();
});
});

it('processWelcomeModalStep(): should get category count', (done) => {
const onboardingRequestResponse: OnboardingWelcomeStepStatus = {
is_complete: true,
};
spenderPlatformV1ApiService.post.and.returnValue(of({ data: onboardingRequestResponse }));

spenderOnboardingService.processWelcomeModalStep(onboardingRequestResponse).subscribe((res) => {
expect(res).toEqual(onboardingRequestResponse);
expect(spenderPlatformV1ApiService.post).toHaveBeenCalledOnceWith('/onboarding/process_step_show_welcome_modal', {
data: onboardingRequestResponse,
});
done();
});
});

it('markWelcomeModalStepAsComplete(): should call processWelcomeModalStep with the correct data', (done) => {
const mockData: OnboardingWelcomeStepStatus = { is_complete: true };
spyOn(spenderOnboardingService, 'processWelcomeModalStep').and.returnValue(of(mockData));

spenderOnboardingService.markWelcomeModalStepAsComplete().subscribe((result) => {
expect(spenderOnboardingService.processWelcomeModalStep).toHaveBeenCalledWith(mockData);
expect(result).toEqual(mockData);
done();
});
});

it('markConnectCardsStepAsComplete(): should call processConnectCardsStep with the correct data', (done) => {
const mockData: OnboardingStepStatus = { is_configured: true, is_skipped: false };
spyOn(spenderOnboardingService, 'processConnectCardsStep').and.returnValue(of(mockData));

spenderOnboardingService.markConnectCardsStepAsComplete().subscribe((result) => {
expect(spenderOnboardingService.processConnectCardsStep).toHaveBeenCalledWith(mockData);
expect(result).toEqual(mockData);
done();
});
});

it('skipConnectCardsStep(): should call processConnectCardsStep with the correct data', (done) => {
const mockData: OnboardingStepStatus = { is_configured: false, is_skipped: true };
spyOn(spenderOnboardingService, 'processConnectCardsStep').and.returnValue(of(mockData));

spenderOnboardingService.skipConnectCardsStep().subscribe((result) => {
expect(spenderOnboardingService.processConnectCardsStep).toHaveBeenCalledWith(mockData);
expect(result).toEqual(mockData);
done();
});
});

it('markSmsOptInStepAsComplete(): should call processSmsOptInStep with the correct data', (done) => {
const mockData: OnboardingStepStatus = { is_configured: true, is_skipped: false };
spyOn(spenderOnboardingService, 'processSmsOptInStep').and.returnValue(of(mockData));

spenderOnboardingService.markSmsOptInStepAsComplete().subscribe((result) => {
expect(spenderOnboardingService.processSmsOptInStep).toHaveBeenCalledWith(mockData);
expect(result).toEqual(mockData);
done();
});
});

it('skipSmsOptInStep(): should call processSmsOptInStep with the correct data', (done) => {
const mockData: OnboardingStepStatus = { is_configured: false, is_skipped: true };
spyOn(spenderOnboardingService, 'processSmsOptInStep').and.returnValue(of(mockData));

spenderOnboardingService.skipSmsOptInStep().subscribe((result) => {
expect(spenderOnboardingService.processSmsOptInStep).toHaveBeenCalledWith(mockData);
expect(result).toEqual(mockData);
done();
});
});
});
13 changes: 7 additions & 6 deletions src/app/core/services/spender-onboarding.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,36 +4,37 @@ import { SpenderPlatformV1ApiService } from './spender-platform-v1-api.service';
import { PlatformApiResponse } from '../models/platform/platform-api-response.model';
import { OnboardingWelcomeStepStatus } from '../models/onboarding-welcome-step-status.model';
import { OnboardingStepStatus } from '../models/onboarding-step-status.model';
import { OnboardingStatus } from '../models/onboarding-status.model';

@Injectable({
providedIn: 'root',
})
export class OnboardingService {
export class SpenderOnboardingService {
constructor(private spenderPlatformV1ApiService: SpenderPlatformV1ApiService) {}

getOnboardingStatus(): Observable<OnboardingStepStatus> {
getOnboardingStatus(): Observable<OnboardingStatus> {
return this.spenderPlatformV1ApiService
.get<PlatformApiResponse<OnboardingStepStatus>>('/spender/onboarding')
.get<PlatformApiResponse<OnboardingStatus>>('/onboarding')
.pipe(map((res) => res.data));
}

processConnectCardsStep(data: OnboardingStepStatus): Observable<OnboardingStepStatus> {
return this.spenderPlatformV1ApiService
.post<PlatformApiResponse<OnboardingStepStatus>>('/spender/onboarding/process_step_connect_cards', {
.post<PlatformApiResponse<OnboardingStepStatus>>('/onboarding/process_step_connect_cards', {
data,
})
.pipe(map((res) => res.data));
}

processSmsOptInStep(data: OnboardingStepStatus): Observable<OnboardingStepStatus> {
return this.spenderPlatformV1ApiService
.post<PlatformApiResponse<OnboardingStepStatus>>('/spender/onboarding/process_step_sms_opt_in', { data })
.post<PlatformApiResponse<OnboardingStepStatus>>('/onboarding/process_step_sms_opt_in', { data })
.pipe(map((res) => res.data));
}

processWelcomeModalStep(data: OnboardingWelcomeStepStatus): Observable<OnboardingWelcomeStepStatus> {
return this.spenderPlatformV1ApiService
.post<PlatformApiResponse<OnboardingWelcomeStepStatus>>('/spender/onboarding/process_step_show_welcome_modal', {
.post<PlatformApiResponse<OnboardingWelcomeStepStatus>>('/onboarding/process_step_show_welcome_modal', {
data,
})
.pipe(map((res) => res.data));
Expand Down
5 changes: 5 additions & 0 deletions src/app/fyle/fyle-routing.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ const routes: Routes = [
path: 'add_edit_expense',
loadChildren: () => import('./add-edit-expense/add-edit-expense.module').then((m) => m.AddEditExpensePageModule),
},
{
path: 'spender_onboarding',
loadChildren: () =>
import('./spender-onboarding/spender-onboarding.module').then((m) => m.SpenderOnboardingPageModule),
},
{
path: 'team_reports',
loadChildren: () => import('./team-reports/team-reports.module').then((m) => m.TeamReportsPageModule),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export enum OnboardingStep {
CONNECT_CARD,
OPT_IN,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
<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>

<form formGroup="fg" *ngIf="fg">
bistaastha marked this conversation as resolved.
Show resolved Hide resolved
<div *ngIf="enrollableCards?.length > 0">
<div *ngFor="let card of enrollableCards; let i = index">
<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': false }"
bistaastha marked this conversation as resolved.
Show resolved Hide resolved
>
<input
class="smartlook-show connect-card__card-number-input pl-0"
inputmode="numeric"
placeholder="xxxx xxxx xxxx"
data-testid="card-number-input"
appAutofocus
[timeout]="500"
[formControlName]="'card_number_' + i"
required
(input)="onCardNumberUpdate(card, 'card_number_' + i)"
/>
bistaastha marked this conversation as resolved.
Show resolved Hide resolved

<div>
{{ card?.card_number || '' }}
</div>

<ion-icon
*ngIf="cardValuesMap?.[card.id].card_type === 'Others'"
src="../../../../assets/svg/card.svg"
class="connect-card__input-default-icon"
data-testid="default-icon"
></ion-icon>

<img
*ngIf="cardValuesMap?.[card.id].card_type === 'Visa'"
src="../../../../assets/images/visa-logo.png"
class="connect-card__input-visa-icon"
data-testid="visa-icon"
/>

<img
*ngIf="cardValuesMap?.[card.id].card_type === '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="fg.controls['card_number_' + i]?.touched && fg.controls['card_number_' + i]?.invalid"
class="add-corporate-card__input-errors"
data-testid="error-message"
>
<span *ngIf="fg.controls['card_number_' + i]?.errors.invalidCardNumber"
>Please enter a valid card number.</span
>

<ng-container *ngIf="fg.controls['card_number_' + i]?.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="
fg.controls['card_number_' + i]?.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="fg.controls['card_number_' + i]?.errors.invalidCardNetwork && isMastercardRTFEnabled"
>Enter a valid Mastercard number. If you have other cards, please contact your admin.</span
>
</ng-template>
</ng-container>
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

Time to simplify these error messages like a boss!

These nested error messages are more complex than my punch dialogues! Let's make them simple and powerful!

Move the error message logic to the component:

// In component
getErrorMessage(index: number): string {
  const control = this.fg.controls[`card_number_${index}`];
  if (control?.errors?.invalidCardNumber) {
    return 'Please enter a valid card number.';
  }
  if (control?.errors?.invalidCardNetwork) {
    if (this.isVisaRTFEnabled && this.isMastercardRTFEnabled) {
      return 'Enter a valid Visa or Mastercard number. If you have other cards, please contact your admin.';
    }
    if (this.isVisaRTFEnabled) {
      return 'Enter a valid Visa number. If you have other cards, please contact your admin.';
    }
    if (this.isMastercardRTFEnabled) {
      return 'Enter a valid Mastercard number. If you have other cards, please contact your admin.';
    }
  }
  return '';
}

Then simplify the template:

- <span *ngIf="fg.controls['card_number_' + i]?.errors.invalidCardNumber">Please enter a valid card number.</span>
- <ng-container *ngIf="fg.controls['card_number_' + i]?.errors.invalidCardNetwork">
-   <!-- Complex nested conditions -->
- </ng-container>
+ <span>{{ getErrorMessage(i) }}</span>

</div>
</div>
</div>
<div *ngIf="enrollableCards.length === 0"></div>
</form>
</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"
(click)="enrollCards()"
>
Continue
</ion-button>
bistaastha marked this conversation as resolved.
Show resolved Hide resolved
</div>
</div>
Loading