Skip to content

Commit 6778282

Browse files
feat: reintroduce smartlook recording in mobile app (#4070)
1 parent b6f3e67 commit 6778282

File tree

6 files changed

+258
-2
lines changed

6 files changed

+258
-2
lines changed

hooks/utils/prod-environment.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export const environment = {
1717
MIXPANEL_PROJECT_TOKEN: '${process.env.MIXPANEL_PROJECT_TOKEN}',
1818
USE_MIXPANEL_PROXY: '${process.env.USE_MIXPANEL_PROXY}',
1919
ENABLE_MIXPANEL: '${process.env.ENABLE_MIXPANEL}',
20-
YODLEE_FAST_LINK_URL: '${process.env.YODLEE_FAST_LINK_URL}'
20+
YODLEE_FAST_LINK_URL: '${process.env.YODLEE_FAST_LINK_URL}',
21+
SMARTLOOK_API_KEY: '${process.env.SMARTLOOK_API_KEY}'
2122
};
2223
`;
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
import { TestBed, fakeAsync, tick } from '@angular/core/testing';
2+
import { Smartlook } from '@awesome-cordova-plugins/smartlook/ngx';
3+
import { of } from 'rxjs';
4+
import { AuthService } from './auth.service';
5+
import { CurrencyService } from './currency.service';
6+
import { DeviceService } from './device.service';
7+
import { NetworkService } from './network.service';
8+
import { extendedDeviceInfoMockData } from '../mock-data/extended-device-info.data';
9+
import { apiEouRes } from '../mock-data/extended-org-user.data';
10+
import { cloneDeep } from 'lodash';
11+
12+
import { SmartlookService } from './smartlook.service';
13+
14+
describe('SmartlookService', () => {
15+
let smartLookService: SmartlookService;
16+
let networkService: jasmine.SpyObj<NetworkService>;
17+
let currencyService: jasmine.SpyObj<CurrencyService>;
18+
let authService: jasmine.SpyObj<AuthService>;
19+
let deviceService: jasmine.SpyObj<DeviceService>;
20+
let smartlook: jasmine.SpyObj<Smartlook>;
21+
22+
beforeEach(() => {
23+
const networkServiceSpy = jasmine.createSpyObj('NetworkService', ['connectivityWatcher', 'isOnline']);
24+
const currencyServiceSpy = jasmine.createSpyObj('CurrencyService', ['getHomeCurrency']);
25+
const authServiceSpy = jasmine.createSpyObj('AuthService', ['getEou']);
26+
const deviceServiceSpy = jasmine.createSpyObj('DeviceService', ['getDeviceInfo']);
27+
const smartlookSpy = jasmine.createSpyObj('Smartlook', [
28+
'setProjectKey',
29+
'start',
30+
'setUserIdentifier',
31+
'setUserProperty',
32+
]);
33+
34+
TestBed.configureTestingModule({
35+
providers: [
36+
SmartlookService,
37+
{ provide: NetworkService, useValue: networkServiceSpy },
38+
{ provide: CurrencyService, useValue: currencyServiceSpy },
39+
{ provide: AuthService, useValue: authServiceSpy },
40+
{ provide: DeviceService, useValue: deviceServiceSpy },
41+
{ provide: Smartlook, useValue: smartlookSpy },
42+
],
43+
});
44+
smartLookService = TestBed.inject(SmartlookService);
45+
networkService = TestBed.inject(NetworkService) as jasmine.SpyObj<NetworkService>;
46+
currencyService = TestBed.inject(CurrencyService) as jasmine.SpyObj<CurrencyService>;
47+
authService = TestBed.inject(AuthService) as jasmine.SpyObj<AuthService>;
48+
deviceService = TestBed.inject(DeviceService) as jasmine.SpyObj<DeviceService>;
49+
smartlook = TestBed.inject(Smartlook) as jasmine.SpyObj<Smartlook>;
50+
});
51+
52+
it('should be created', () => {
53+
expect(smartLookService).toBeTruthy();
54+
});
55+
56+
it('setupNetworkWatcher(): should setup a network watcher', () => {
57+
const emitterSpy = jasmine.createSpyObj('EventEmitter', ['asObservable']);
58+
emitterSpy.asObservable.and.returnValue(of(true));
59+
smartLookService.setupNetworkWatcher();
60+
networkService.isOnline.and.returnValue(of(true));
61+
expect(networkService.connectivityWatcher).toHaveBeenCalledTimes(2);
62+
expect(networkService.isOnline).toHaveBeenCalledTimes(2);
63+
});
64+
65+
describe('init():', () => {
66+
beforeEach(() => {
67+
networkService.isOnline.and.returnValue(of(true));
68+
smartLookService.setupNetworkWatcher();
69+
});
70+
71+
it('should initialize Smartlook when all conditions are met', fakeAsync(() => {
72+
const mockEou = cloneDeep(apiEouRes);
73+
mockEou.us.email = '[email protected]';
74+
75+
currencyService.getHomeCurrency.and.returnValue(of('USD'));
76+
authService.getEou.and.resolveTo(mockEou);
77+
deviceService.getDeviceInfo.and.returnValue(of(extendedDeviceInfoMockData));
78+
79+
smartLookService.init();
80+
tick();
81+
82+
expect(smartlook.setProjectKey).toHaveBeenCalledTimes(1);
83+
expect(smartlook.start).toHaveBeenCalledTimes(1);
84+
expect(smartlook.setUserIdentifier).toHaveBeenCalledOnceWith({ identifier: mockEou.us.id });
85+
expect(smartlook.setUserProperty).toHaveBeenCalledWith({ propertyName: 'id', value: mockEou.us.id });
86+
expect(smartlook.setUserProperty).toHaveBeenCalledWith({
87+
propertyName: 'org_id',
88+
value: mockEou.ou.org_id,
89+
});
90+
expect(smartlook.setUserProperty).toHaveBeenCalledWith({
91+
propertyName: 'devicePlatform',
92+
value: extendedDeviceInfoMockData.platform,
93+
});
94+
expect(smartlook.setUserProperty).toHaveBeenCalledWith({
95+
propertyName: 'deviceModel',
96+
value: extendedDeviceInfoMockData.model,
97+
});
98+
expect(smartlook.setUserProperty).toHaveBeenCalledWith({
99+
propertyName: 'deviceOS',
100+
value: extendedDeviceInfoMockData.osVersion,
101+
});
102+
expect(smartlook.setUserProperty).toHaveBeenCalledWith({ propertyName: 'is_approver', value: 'true' });
103+
}));
104+
105+
it('should set is_approver to false when user does not have APPROVER role', fakeAsync(() => {
106+
const mockEou = cloneDeep(apiEouRes);
107+
mockEou.us.email = '[email protected]';
108+
mockEou.ou.roles = ['FYLER'];
109+
110+
currencyService.getHomeCurrency.and.returnValue(of('USD'));
111+
authService.getEou.and.resolveTo(mockEou);
112+
deviceService.getDeviceInfo.and.returnValue(of(extendedDeviceInfoMockData));
113+
114+
smartLookService.init();
115+
tick();
116+
117+
expect(smartlook.setUserProperty).toHaveBeenCalledWith({ propertyName: 'is_approver', value: 'false' });
118+
}));
119+
120+
it('should not initialize Smartlook when user is offline', fakeAsync(() => {
121+
const mockEou = cloneDeep(apiEouRes);
122+
mockEou.us.email = '[email protected]';
123+
124+
networkService.isOnline.and.returnValue(of(false));
125+
smartLookService.setupNetworkWatcher();
126+
currencyService.getHomeCurrency.and.returnValue(of('USD'));
127+
authService.getEou.and.resolveTo(mockEou);
128+
deviceService.getDeviceInfo.and.returnValue(of(extendedDeviceInfoMockData));
129+
130+
smartLookService.init();
131+
tick();
132+
133+
expect(smartlook.setProjectKey).not.toHaveBeenCalled();
134+
expect(smartlook.start).not.toHaveBeenCalled();
135+
}));
136+
137+
it('should not initialize Smartlook when user email contains fyle', fakeAsync(() => {
138+
currencyService.getHomeCurrency.and.returnValue(of('USD'));
139+
authService.getEou.and.resolveTo(apiEouRes);
140+
deviceService.getDeviceInfo.and.returnValue(of(extendedDeviceInfoMockData));
141+
142+
smartLookService.init();
143+
tick();
144+
145+
expect(smartlook.setProjectKey).not.toHaveBeenCalled();
146+
expect(smartlook.start).not.toHaveBeenCalled();
147+
}));
148+
149+
it('should not initialize Smartlook when home currency is not USD', fakeAsync(() => {
150+
const mockEou = cloneDeep(apiEouRes);
151+
mockEou.us.email = '[email protected]';
152+
153+
currencyService.getHomeCurrency.and.returnValue(of('INR'));
154+
authService.getEou.and.resolveTo(mockEou);
155+
deviceService.getDeviceInfo.and.returnValue(of(extendedDeviceInfoMockData));
156+
157+
smartLookService.init();
158+
tick();
159+
160+
expect(smartlook.setProjectKey).not.toHaveBeenCalled();
161+
expect(smartlook.start).not.toHaveBeenCalled();
162+
}));
163+
164+
it('should not initialize Smartlook when eou is null', fakeAsync(() => {
165+
currencyService.getHomeCurrency.and.returnValue(of('USD'));
166+
authService.getEou.and.resolveTo(null);
167+
deviceService.getDeviceInfo.and.returnValue(of(extendedDeviceInfoMockData));
168+
169+
smartLookService.init();
170+
tick();
171+
172+
expect(smartlook.setProjectKey).not.toHaveBeenCalled();
173+
expect(smartlook.start).not.toHaveBeenCalled();
174+
}));
175+
});
176+
});
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { Injectable, EventEmitter, inject } from '@angular/core';
2+
import { forkJoin, from, concat, Observable } from 'rxjs';
3+
import { filter, take } from 'rxjs/operators';
4+
import { AuthService } from './auth.service';
5+
import { CurrencyService } from './currency.service';
6+
import { DeviceService } from './device.service';
7+
import { Smartlook } from '@awesome-cordova-plugins/smartlook/ngx';
8+
import { environment } from 'src/environments/environment';
9+
import { NetworkService } from './network.service';
10+
11+
@Injectable({
12+
providedIn: 'root',
13+
})
14+
export class SmartlookService {
15+
private currencyService = inject(CurrencyService);
16+
17+
private authService = inject(AuthService);
18+
19+
private deviceService = inject(DeviceService);
20+
21+
private networkService = inject(NetworkService);
22+
23+
private smartlook = inject(Smartlook);
24+
25+
isConnected$: Observable<boolean>;
26+
27+
constructor() {
28+
this.setupNetworkWatcher();
29+
}
30+
31+
setupNetworkWatcher(): void {
32+
const networkWatcherEmitter = new EventEmitter<boolean>();
33+
this.networkService.connectivityWatcher(networkWatcherEmitter);
34+
this.isConnected$ = concat(this.networkService.isOnline(), networkWatcherEmitter.asObservable());
35+
}
36+
37+
init(): void {
38+
forkJoin({
39+
isConnected: this.isConnected$.pipe(take(1)),
40+
homeCurrency: this.currencyService.getHomeCurrency(),
41+
eou: from(this.authService.getEou()),
42+
deviceInfo: this.deviceService.getDeviceInfo(),
43+
})
44+
.pipe(
45+
filter(
46+
({ isConnected, homeCurrency, eou }) =>
47+
isConnected && eou && !eou.us.email.includes('fyle') && homeCurrency === 'USD',
48+
),
49+
)
50+
.subscribe(({ eou, deviceInfo }) => {
51+
this.smartlook.setProjectKey({ key: environment.SMARTLOOK_API_KEY });
52+
this.smartlook.start();
53+
this.smartlook.setUserIdentifier({ identifier: eou.us.id });
54+
this.smartlook.setUserProperty({ propertyName: 'id', value: eou.us.id });
55+
this.smartlook.setUserProperty({ propertyName: 'org_id', value: eou.ou.org_id });
56+
this.smartlook.setUserProperty({ propertyName: 'devicePlatform', value: deviceInfo.platform });
57+
this.smartlook.setUserProperty({ propertyName: 'deviceModel', value: deviceInfo.model });
58+
this.smartlook.setUserProperty({ propertyName: 'deviceOS', value: deviceInfo.osVersion });
59+
this.smartlook.setUserProperty({
60+
propertyName: 'is_approver',
61+
value: eou.ou.roles.includes('APPROVER') ? 'true' : 'false',
62+
});
63+
});
64+
}
65+
}

src/app/fyle/dashboard/dashboard.page.spec.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ import { FyMenuIconComponent } from 'src/app/shared/components/fy-menu-icon/fy-m
6363
import { DashboardEmailOptInComponent } from 'src/app/shared/components/dashboard-email-opt-in/dashboard-email-opt-in.component';
6464
import { DashboardOptInComponent } from 'src/app/shared/components/dashboard-opt-in/dashboard-opt-in.component';
6565
import { getFormatPreferenceProviders } from 'src/app/core/testing/format-preference-providers.utils';
66+
import { SmartlookService } from 'src/app/core/services/smartlook.service';
6667

6768
// mocks
6869
@Component({
@@ -119,6 +120,7 @@ describe('DashboardPage', () => {
119120
let popoverController: jasmine.SpyObj<PopoverController>;
120121
let launchDarklyService: jasmine.SpyObj<LaunchDarklyService>;
121122
let budgetsService: jasmine.SpyObj<BudgetsService>;
123+
let smartlookService: jasmine.SpyObj<SmartlookService>;
122124
beforeEach(waitForAsync(() => {
123125
const networkServiceSpy = jasmine.createSpyObj('NetworkService', ['connectivityWatcher', 'isOnline']);
124126
const currencyServiceSpy = jasmine.createSpyObj('CurrencyService', ['getHomeCurrency']);
@@ -178,6 +180,7 @@ describe('DashboardPage', () => {
178180
const orgUserServiceSpy = jasmine.createSpyObj('OrgUserService', ['getDwollaCustomer']);
179181
const launchDarklyServiceSpy = jasmine.createSpyObj('LaunchDarklyService', ['getVariation']);
180182
const budgetsServiceSpy = jasmine.createSpyObj('BudgetsService', ['getSpenderBudgetByParams']);
183+
const smartlookServiceSpy = jasmine.createSpyObj('SmartlookService', ['init']);
181184
TestBed.configureTestingModule({
182185
imports: [DashboardPage, MatIconTestingModule, getTranslocoTestingModule()],
183186
providers: [
@@ -239,6 +242,10 @@ describe('DashboardPage', () => {
239242
provide: BudgetsService,
240243
useValue: budgetsServiceSpy,
241244
},
245+
{
246+
provide: SmartlookService,
247+
useValue: smartlookServiceSpy,
248+
},
242249
...getFormatPreferenceProviders(),
243250
],
244251
schemas: [NO_ERRORS_SCHEMA],
@@ -298,6 +305,7 @@ describe('DashboardPage', () => {
298305
launchDarklyService = TestBed.inject(LaunchDarklyService) as jasmine.SpyObj<LaunchDarklyService>;
299306
budgetsService = TestBed.inject(BudgetsService) as jasmine.SpyObj<BudgetsService>;
300307
budgetsService.getSpenderBudgetByParams.and.returnValue(of([]));
308+
smartlookService = TestBed.inject(SmartlookService) as jasmine.SpyObj<SmartlookService>;
301309
fixture.detectChanges();
302310
}));
303311

@@ -410,10 +418,11 @@ describe('DashboardPage', () => {
410418
featureConfigService.saveConfiguration.and.returnValue(of(null));
411419
});
412420

413-
it('should call setupNetworkWatcher, registerBackButtonAction once', () => {
421+
it('should call setupNetworkWatcher, registerBackButtonAction once, smartlookService.init once', () => {
414422
component.ionViewWillEnter();
415423
expect(component.setupNetworkWatcher).toHaveBeenCalledTimes(1);
416424
expect(component.registerBackButtonAction).toHaveBeenCalledTimes(1);
425+
expect(smartlookService.init).toHaveBeenCalledTimes(1);
417426
});
418427

419428
it('should set currentStateIndex to 1 if queryParams.state is tasks', () => {

src/app/fyle/dashboard/dashboard.page.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ import { PopupAlertComponent } from 'src/app/shared/components/popup-alert/popup
7070
import { OverlayEventDetail, SegmentCustomEvent } from '@ionic/core';
7171
import { Budget } from 'src/app/core/models/budget.model';
7272
import { BudgetsService } from 'src/app/core/services/platform/v1/spender/budgets.service';
73+
import { SmartlookService } from 'src/app/core/services/smartlook.service';
7374

7475
// install Swiper modules
7576
SwiperCore.use([Pagination, Autoplay]);
@@ -158,6 +159,8 @@ export class DashboardPage {
158159

159160
private launchDarklyService = inject(LaunchDarklyService);
160161

162+
private smartlookService = inject(SmartlookService);
163+
161164
private popoverController = inject(PopoverController);
162165

163166
private budgetsService = inject(BudgetsService);
@@ -690,6 +693,7 @@ export class DashboardPage {
690693
};
691694
this.setupNetworkWatcher();
692695
this.registerBackButtonAction();
696+
this.smartlookService.init();
693697
this.footerService.footerCurrentStateIndex$.subscribe((index) => {
694698
this.currentStateIndex = index;
695699
});

src/environments/environment.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export const environment = {
1919
USE_MIXPANEL_PROXY: '',
2020
ENABLE_MIXPANEL: '',
2121
YODLEE_FAST_LINK_URL: '',
22+
SMARTLOOK_API_KEY: '',
2223
};
2324

2425
/*

0 commit comments

Comments
 (0)