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: new integrations landing page (/landing_v2) #1151

Merged
merged 2 commits into from
Jan 17, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
5 changes: 4 additions & 1 deletion src/app/core/models/enum/enum.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ export enum InAppIntegration {
QBD_DIRECT = 'QuickBooks Connector'
}

export type IntegrationAppKey = keyof typeof InAppIntegration;

export enum ToastSeverity {
SUCCESS = 'success',
ERROR = 'error',
Expand Down Expand Up @@ -900,7 +902,8 @@ export enum SizeOption {
export enum ThemeOption {
BRAND = 'brand',
LIGHT = 'light',
DARK = 'dark'
DARK = 'dark',
SUCCESS = 'success'
}

export enum QBDPreRequisiteState {
Expand Down
11 changes: 11 additions & 0 deletions src/app/core/models/integrations/integrations.model.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
import { AccountingIntegrationApp, AppUrl, ClickEvent, InAppIntegration, IntegrationView } from "../enum/enum.model";

export type Integration = {
id: number;
org_id: string;
org_name: string;
tpa_id: string;
tpa_name: string;
type: string;
is_active: boolean;
is_beta: boolean;
}

export type IntegrationsView = {
[IntegrationView.ACCOUNTING]: boolean,
[IntegrationView.ALL]: boolean,
Expand Down
16 changes: 16 additions & 0 deletions src/app/core/services/common/integrations.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';

import { IntegrationsService } from './integrations.service';

xdescribe('IntegrationsService', () => {
let service: IntegrationsService;

beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(IntegrationsService);
});

it('should be created', () => {
expect(service).toBeTruthy();
});
});
Comment on lines +5 to +16
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Enable the test suite by replacing xdescribe with describe.

The test suite is currently disabled due to the xdescribe. To ensure the tests run, remove the x prefix.

Apply this diff to enable the tests:

- xdescribe('IntegrationsService', () => {
+ describe('IntegrationsService', () => {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
xdescribe('IntegrationsService', () => {
let service: IntegrationsService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(IntegrationsService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});
describe('IntegrationsService', () => {
let service: IntegrationsService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(IntegrationsService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

22 changes: 22 additions & 0 deletions src/app/core/services/common/integrations.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Injectable } from '@angular/core';
import { AppUrl } from '../../models/enum/enum.model';
import { ApiService } from './api.service';
import { HelperService } from './helper.service';
import { Observable } from 'rxjs';
import { Integration } from '../../models/integrations/integrations.model';

@Injectable({
providedIn: 'root'
})
export class IntegrationsService {
constructor(
private apiService: ApiService,
private helper: HelperService
) {
}

getIntegrations(): Observable<Integration[]> {
this.helper.setBaseApiURL(AppUrl.INTEGRATION);
return this.apiService.get(`/integrations/`, {});
}
Comment on lines +18 to +21
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add error handling and consider caching.

The getIntegrations method should handle API errors and implement caching for better performance.

Consider implementing:

+ private integrationsCache$ = new ReplaySubject<Integration[]>(1);

  getIntegrations(): Observable<Integration[]> {
    this.helper.setBaseApiURL(AppUrl.INTEGRATION);
-    return this.apiService.get(`/integrations/`, {});
+    return this.apiService.get<Integration[]>(`/integrations/`, {}).pipe(
+      catchError(error => {
+        console.error('Failed to fetch integrations:', error);
+        return throwError(() => new Error('Failed to fetch integrations. Please try again.'));
+      }),
+      tap(integrations => this.integrationsCache$.next(integrations)),
+      shareReplay(1)
+    );
  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
getIntegrations(): Observable<Integration[]> {
this.helper.setBaseApiURL(AppUrl.INTEGRATION);
return this.apiService.get(`/integrations/`, {});
}
private integrationsCache$ = new ReplaySubject<Integration[]>(1);
getIntegrations(): Observable<Integration[]> {
this.helper.setBaseApiURL(AppUrl.INTEGRATION);
return this.apiService.get<Integration[]>(`/integrations/`, {}).pipe(
catchError(error => {
console.error('Failed to fetch integrations:', error);
return throwError(() => new Error('Failed to fetch integrations. Please try again.'));
}),
tap(integrations => this.integrationsCache$.next(integrations)),
shareReplay(1)
);
}

}
5 changes: 5 additions & 0 deletions src/app/integrations/integrations-routing.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { IntegrationsComponent } from './integrations.component';
import { LandingComponent } from './landing/landing.component';
import { LandingV2Component } from './landing-v2/landing-v2.component';

const routes: Routes = [
{
Expand All @@ -12,6 +13,10 @@ const routes: Routes = [
path: 'landing',
component: LandingComponent
},
{
path: 'landing_v2',
component: LandingV2Component
},
{
path: 'bamboo_hr',
loadChildren: () => import('./bamboo-hr/bamboo-hr.module').then(m => m.BambooHrModule)
Expand Down
4 changes: 3 additions & 1 deletion src/app/integrations/integrations.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { SharedModule } from '../shared/shared.module';
import { Sage300Component } from './sage300/sage300.component';
import { XeroComponent } from './xero/xero.component';
import { TravelperkComponent } from './travelperk/travelperk.component';
import { LandingV2Component } from './landing-v2/landing-v2.component';

@NgModule({
declarations: [
Expand All @@ -18,7 +19,8 @@ import { TravelperkComponent } from './travelperk/travelperk.component';
QbdComponent,
Sage300Component,
XeroComponent,
TravelperkComponent
TravelperkComponent,
LandingV2Component
],
imports: [
CommonModule,
Expand Down
239 changes: 239 additions & 0 deletions src/app/integrations/landing-v2/landing-v2.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
<div class="tw-text-slightly-normal-text-color tw-py-8 tw-px-6">
<div class="landing-v2--container">
<header>
<h3 class="landing-v2--section-heading">
List of integrations
</h3>

<p class="landing-v2--section-caption">
If your company uses any of the applications listed below, you can easily integrate them with Fyle.
<br>
Need an integration we don't support yet? Let us know at <a class="tw-text-link-primary"
href="mailto:[email protected]">support{{'@'}}fylehq.com</a>
</p>
</header>
<div *ngIf="!exposeC1Apps" class="tw-flex tw-text-14-px tw-border-y tw-border-separator">
<div class="landing-v2--tab" [ngClass]="{'tw-text-menu-inactive-text-color': !integrationTabs.ALL}"
(click)="switchView(IntegrationsView.ALL)">
All
<p *ngIf="integrationTabs.ALL" class="landing-v2--active-tag"></p>
</div>
<div class="landing-v2--tab" [ngClass]="{'tw-text-menu-inactive-text-color': !integrationTabs.ACCOUNTING}"
(click)="switchView(IntegrationsView.ACCOUNTING)">
Accounting
<p *ngIf="integrationTabs.ACCOUNTING" class="landing-v2--active-tag"></p>
</div>
<div class="landing-v2--tab" [ngClass]="{'tw-text-menu-inactive-text-color': !integrationTabs.HRMS}"
(click)="switchView(IntegrationsView.HRMS)">
HRMS
<p *ngIf="integrationTabs.HRMS" class="landing-v2--active-tag"></p>
</div>
<div *ngIf="org.allow_travelperk" class="landing-v2--tab"
[ngClass]="{'tw-text-menu-inactive-text-color': !integrationTabs.TRAVEL}"
(click)="switchView(IntegrationsView.TRAVEL)">
Travel
<p *ngIf="integrationTabs.TRAVEL" class="landing-v2--active-tag"></p>
</div>
</div>
Comment on lines +15 to +37
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add ARIA labels for better accessibility.

The tab navigation needs proper ARIA attributes for better screen reader support.

-        <div *ngIf="!exposeC1Apps" class="tw-flex tw-text-14-px tw-border-y tw-border-separator">
+        <div *ngIf="!exposeC1Apps" class="tw-flex tw-text-14-px tw-border-y tw-border-separator" role="tablist" aria-label="Integration categories">
-            <div class="landing-v2--tab"
+            <div class="landing-v2--tab" role="tab" [attr.aria-selected]="integrationTabs.ALL" [attr.tabindex]="integrationTabs.ALL ? 0 : -1"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<div *ngIf="!exposeC1Apps" class="tw-flex tw-text-14-px tw-border-y tw-border-separator">
<div class="landing-v2--tab" [ngClass]="{'tw-text-menu-inactive-text-color': !integrationTabs.ALL}"
(click)="switchView(IntegrationsView.ALL)">
All
<p *ngIf="integrationTabs.ALL" class="landing-v2--active-tag"></p>
</div>
<div class="landing-v2--tab" [ngClass]="{'tw-text-menu-inactive-text-color': !integrationTabs.ACCOUNTING}"
(click)="switchView(IntegrationsView.ACCOUNTING)">
Accounting
<p *ngIf="integrationTabs.ACCOUNTING" class="landing-v2--active-tag"></p>
</div>
<div class="landing-v2--tab" [ngClass]="{'tw-text-menu-inactive-text-color': !integrationTabs.HRMS}"
(click)="switchView(IntegrationsView.HRMS)">
HRMS
<p *ngIf="integrationTabs.HRMS" class="landing-v2--active-tag"></p>
</div>
<div *ngIf="org.allow_travelperk" class="landing-v2--tab"
[ngClass]="{'tw-text-menu-inactive-text-color': !integrationTabs.TRAVEL}"
(click)="switchView(IntegrationsView.TRAVEL)">
Travel
<p *ngIf="integrationTabs.TRAVEL" class="landing-v2--active-tag"></p>
</div>
</div>
<div *ngIf="!exposeC1Apps" class="tw-flex tw-text-14-px tw-border-y tw-border-separator" role="tablist" aria-label="Integration categories">
<div class="landing-v2--tab" role="tab" [attr.aria-selected]="integrationTabs.ALL" [attr.tabindex]="integrationTabs.ALL ? 0 : -1"
[ngClass]="{'tw-text-menu-inactive-text-color': !integrationTabs.ALL}"
(click)="switchView(IntegrationsView.ALL)">
All
<p *ngIf="integrationTabs.ALL" class="landing-v2--active-tag"></p>
</div>
<div class="landing-v2--tab" [ngClass]="{'tw-text-menu-inactive-text-color': !integrationTabs.ACCOUNTING}"
(click)="switchView(IntegrationsView.ACCOUNTING)">
Accounting
<p *ngIf="integrationTabs.ACCOUNTING" class="landing-v2--active-tag"></p>
</div>
<div class="landing-v2--tab" [ngClass]="{'tw-text-menu-inactive-text-color': !integrationTabs.HRMS}"
(click)="switchView(IntegrationsView.HRMS)">
HRMS
<p *ngIf="integrationTabs.HRMS" class="landing-v2--active-tag"></p>
</div>
<div *ngIf="org.allow_travelperk" class="landing-v2--tab"
[ngClass]="{'tw-text-menu-inactive-text-color': !integrationTabs.TRAVEL}"
(click)="switchView(IntegrationsView.TRAVEL)">
Travel
<p *ngIf="integrationTabs.TRAVEL" class="landing-v2--active-tag"></p>
</div>
</div>


<div
class="tw-grid tw-grid-cols-2 md:tw-grid-cols-3 lg:tw-grid-cols-4 xl:tw-grid-cols-5 tw-gap-4 tw-justify-items-stretch">
<div *ngIf="isAppShown('NETSUITE')">
<div class="landing-v2--accounting-app"
(click)="openAccountingIntegrationApp(AccountingIntegrationApp.NETSUITE)">
<div class="tw-flex tw-justify-between tw-items-center">
<img src="assets/logos/netsuite-logo-new.png" />
@if (isAppConnected('NETSUITE')) {
<app-badge text="Connected" [theme]="ThemeOption.SUCCESS"></app-badge>
} @else {
<button class="btn-connect">Connect</button>
}
</div>
<div>
<span class="landing-v2--accounting-app-name">
NetSuite
</span>
<span class="landing-v2--accounting-app-type">
Accounting
</span>
</div>
</div>
</div>
<div *ngIf="isAppShown('INTACCT')" class="landing-v2--accounting-app"
(click)="openAccountingIntegrationApp(AccountingIntegrationApp.SAGE_INTACCT)">
<div class="tw-flex tw-justify-between tw-items-center">
<img src="assets/logos/intacct-logo-new.png" />
@if (isAppConnected('INTACCT')) {
<app-badge text="Connected" [theme]="ThemeOption.SUCCESS"></app-badge>
} @else {
<button class="btn-connect">Connect</button>
}
</div>
<div>
<span class="landing-v2--accounting-app-name">
Sage Intacct
</span>
<span class="landing-v2--accounting-app-type">
Accounting
</span>
</div>
</div>
<div *ngIf="isAppShown('QBO')" class="landing-v2--accounting-app"
(click)="openAccountingIntegrationApp(AccountingIntegrationApp.QBO)">
<div class="tw-flex tw-justify-between tw-items-center">
<img src="assets/logos/quickbooks-logo.png" class="!tw-h-[30.7px]" />
@if (isAppConnected('QBO')) {
<app-badge text="Connected" [theme]="ThemeOption.SUCCESS"></app-badge>
} @else {
<button class="btn-connect">Connect</button>
}
</div>
<div>
<span class="landing-v2--accounting-app-name">
QuickBooks Online
</span>
<span class="landing-v2--accounting-app-type">
Accounting
</span>
</div>
</div>
<div *ngIf="isAppShown('XERO')" class="landing-v2--accounting-app"
(click)="openAccountingIntegrationApp(AccountingIntegrationApp.XERO)">
<div class="tw-flex tw-justify-between tw-items-center">
<img src="assets/logos/xero-logo-new.png" />
@if (isAppConnected('XERO')) {
<app-badge text="Connected" [theme]="ThemeOption.SUCCESS"></app-badge>
} @else {
<button class="btn-connect">Connect</button>
}
</div>
<div>
<span class="landing-v2--accounting-app-name">
Xero
</span>
<span class="landing-v2--accounting-app-type">
Accounting
</span>
</div>
</div>
<div *ngIf="isAppShown('QBD')" class="landing-v2--accounting-app"
(click)="openInAppIntegration(InAppIntegration.QBD)">
<div class="tw-flex tw-justify-between tw-items-center">
<img src="assets/logos/quickbooks-logo.png" class="!tw-h-[30.7px]" />
@if (isAppConnected('QBD')) {
<app-badge text="Connected" [theme]="ThemeOption.SUCCESS"></app-badge>
} @else {
<button class="btn-connect">Connect</button>
}
</div>
<div>
<span class="landing-v2--accounting-app-name">
QuickBooks Desktop (IIF)
</span>
<span class="landing-v2--accounting-app-type">
Accounting
</span>
</div>
</div>
<!-- Direct -->
<div *ngIf="isAppShown('QBD_DIRECT')" class="landing-v2--accounting-app"
(click)="openInAppIntegration(InAppIntegration.QBD_DIRECT)">
<div class="tw-flex tw-justify-between tw-items-center">
<img src="assets/logos/quickbooks-logo.png" class="!tw-h-[30.7px]" />
@if (isAppConnected('QBD_DIRECT')) {
<app-badge text="Connected" [theme]="ThemeOption.SUCCESS"></app-badge>
} @else {
<button class="btn-connect">Connect</button>
}
</div>
<span class="landing-v2--accounting-app-name tw-items-center tw-gap-4">
<div>
QuickBooks Desktop (Web Connector)
<div class="landing-v2--accounting-app-type">Accounting</div>
</div>
<app-badge [theme]="ThemeOption.DARK" text="Beta"></app-badge>
</span>
</div>
Comment on lines +41 to +156
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Extract integration card to a separate component.

The integration card pattern is repeated multiple times. Consider extracting it to a reusable component to reduce code duplication and improve maintainability.

Create a new component IntegrationCardComponent:

@Component({
  selector: 'app-integration-card',
  template: `
    <div class="landing-v2--accounting-app" (click)="onCardClick()">
      <div class="tw-flex tw-justify-between tw-items-center">
        <img [src]="logoSrc" [class]="logoClass" />
        <app-badge *ngIf="isConnected" text="Connected" [theme]="ThemeOption.SUCCESS"></app-badge>
        <button *ngIf="!isConnected" class="btn-connect">Connect</button>
      </div>
      <div>
        <span class="landing-v2--accounting-app-name">
          {{ name }}
        </span>
        <span class="landing-v2--accounting-app-type">
          {{ type }}
        </span>
      </div>
    </div>
  `
})
export class IntegrationCardComponent {
  @Input() name: string;
  @Input() type: string;
  @Input() logoSrc: string;
  @Input() logoClass: string;
  @Input() isConnected: boolean;
  @Output() connect = new EventEmitter<void>();
}

<div *ngIf="isAppShown('SAGE300')" class="landing-v2--accounting-app"
(click)="openInAppIntegration(InAppIntegration.SAGE300)">
<div class="tw-flex tw-justify-between tw-items-center">
<img src="assets/logos/sage300-logo.png" class="tw-py-[4px]" />
@if (isAppConnected('SAGE300')) {
<app-badge text="Connected" [theme]="ThemeOption.SUCCESS"></app-badge>
} @else {
<button class="btn-connect">Connect</button>
}
</div>
<span class="landing-v2--accounting-app-name tw-items-center tw-gap-4">
<div>
Sage 300 CRE
<div class="landing-v2--accounting-app-type">Accounting</div>
</div>
<app-badge *ngIf="!orgsToHideSage300BetaBadge.includes(org.fyle_org_id)" [theme]="ThemeOption.DARK"
text="Beta"></app-badge>
</span>
</div>
<div *ngIf="isAppShown('BUSINESS_CENTRAL')" class="landing-v2--accounting-app"
(click)="openInAppIntegration(InAppIntegration.BUSINESS_CENTRAL)">
<div class="tw-flex tw-justify-between tw-items-center">
<img src="assets/logos/BusinessCentral-logo.svg" />
@if (isAppConnected('BUSINESS_CENTRAL')) {
<app-badge text="Connected" [theme]="ThemeOption.SUCCESS"></app-badge>
} @else {
<button class="btn-connect">Connect</button>
}
</div>
<span class="landing-v2--accounting-app-name tw-items-center tw-gap-4">
<div>
Dynamics 365 Business Central
<div class="landing-v2--accounting-app-type">Accounting</div>
</div>
<app-badge *ngIf="!orgsToHideBusinessCentralBetaBadge.includes(org.fyle_org_id)"
[theme]="ThemeOption.DARK" text="Beta"></app-badge>
</span>
</div>

<div *ngIf="isAppShown('BAMBOO_HR')">
<div class="landing-v2--accounting-app" (click)="openInAppIntegration(InAppIntegration.BAMBOO_HR)">
<div class="tw-flex tw-justify-between tw-items-center">
<img src="assets/logos/bamboo-hr-logo.png" />
@if (isAppConnected('BAMBOO_HR')) {
<app-badge text="Connected" [theme]="ThemeOption.SUCCESS"></app-badge>
} @else {
<button class="btn-connect">Connect</button>
}
</div>
<div>
<span class="landing-v2--accounting-app-name">
BambooHR
</span>
<span class="landing-v2--accounting-app-type">
HRMS
</span>
</div>
</div>
</div>

<div *ngIf="isAppShown('TRAVELPERK')">
<div class="landing-v2--accounting-app" (click)="openInAppIntegration(InAppIntegration.TRAVELPERK)">
<div class="tw-flex tw-justify-between tw-items-center">
<img src="assets/logos/travelperk-logo.png" class="tw-py-[5px]" />
@if (isAppConnected('TRAVELPERK')) {
<app-badge text="Connected" [theme]="ThemeOption.SUCCESS"></app-badge>
} @else {
<button class="btn-connect">Connect</button>
}
</div>
<div>
<span class="landing-v2--accounting-app-name">
TravelPerk
</span>
<span class="landing-v2--accounting-app-type">
Travel
</span>
</div>
</div>
</div>
</div>
</div>
</div>
Loading
Loading