Skip to content
Open
Show file tree
Hide file tree
Changes from 46 commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
082e946
Minor
bistaastha Jan 21, 2026
03f0a14
Minor
bistaastha Jan 21, 2026
f5879c7
Update org-user.service.spec.ts
bistaastha Jan 21, 2026
2e73a08
Minor
bistaastha Jan 21, 2026
060f63f
Merge branch 'fyle-api-integration-for-notifs' of github.com:fylein/f…
bistaastha Jan 21, 2026
178a224
Minor
bistaastha Jan 22, 2026
ce1f560
Minor
bistaastha Jan 22, 2026
249829c
Minor
bistaastha Jan 22, 2026
e112f85
Minor
bistaastha Jan 22, 2026
b8832ae
Minor
bistaastha Jan 22, 2026
56ea99a
Minor
bistaastha Jan 23, 2026
be63e04
Minor
bistaastha Jan 23, 2026
52554fb
Minor
bistaastha Jan 23, 2026
5897bb6
Merge branch 'fyle-fix-setup-keys' into fyle-add-redirection-for-noti…
bistaastha Jan 23, 2026
d787bac
Merge remote-tracking branch 'origin/fyle-fix-setup-keys' into fyle-a…
bistaastha Jan 23, 2026
b26d6e0
Merge branch 'fyle-fix-setup-keys' into fyle-add-redirection-for-noti…
bistaastha Jan 23, 2026
88cfed2
Merge branch 'fyle-fix-setup-keys' into fyle-add-redirection-for-noti…
bistaastha Jan 23, 2026
f6ec718
Minor
bistaastha Jan 27, 2026
a440a25
Minor
bistaastha Jan 27, 2026
7cf5cfa
Minor
bistaastha Jan 27, 2026
ccbf3f7
Minor
bistaastha Jan 27, 2026
2ce020f
Minor
bistaastha Jan 28, 2026
8d22251
Update push-notification.service.ts
bistaastha Jan 28, 2026
53013f3
Minor
bistaastha Jan 29, 2026
448562f
Minor
bistaastha Jan 29, 2026
2d25b9b
Merge branch 'fyle-add-redirection-for-notifications' of github.com:f…
bistaastha Jan 29, 2026
2b867db
Minor
bistaastha Jan 29, 2026
f418f33
Minor
bistaastha Jan 29, 2026
3462fde
Minor
bistaastha Jan 29, 2026
82f5600
Minor
bistaastha Jan 29, 2026
f070816
Minor
bistaastha Jan 30, 2026
721d8ac
Minor
bistaastha Jan 30, 2026
c0ba3eb
Minor
bistaastha Jan 30, 2026
0815453
Minor
bistaastha Jan 30, 2026
d4f18d1
Minor
bistaastha Jan 30, 2026
50e8495
Minor
bistaastha Jan 30, 2026
7750455
Minor
bistaastha Jan 30, 2026
bc25593
Minor
bistaastha Jan 30, 2026
a575594
Minor
bistaastha Jan 30, 2026
8d3a4ae
Minor
bistaastha Jan 30, 2026
4cbbf17
Minor
bistaastha Jan 30, 2026
d21b725
Minor
bistaastha Jan 30, 2026
afaa507
Minor
bistaastha Jan 30, 2026
e55921e
Minor
bistaastha Jan 30, 2026
b37f3c8
Minor
bistaastha Jan 30, 2026
b32f422
Minor
bistaastha Jan 31, 2026
fc141cf
Minor
bistaastha Jan 31, 2026
ba510e3
Minor
bistaastha Jan 31, 2026
adb990b
Minor
bistaastha Jan 31, 2026
ca32a7a
Minor
bistaastha Jan 31, 2026
fe356a4
Minor
bistaastha Jan 31, 2026
a44f505
Minor
bistaastha Feb 1, 2026
67d5a95
Merge remote-tracking branch 'origin/fyle-fix-setup-keys' into fyle-a…
amit429 Feb 2, 2026
99782e3
testing
amit429 Feb 2, 2026
2603fdc
Commented get device token logic for testing purposes
amit429 Feb 4, 2026
6424719
testing the deeplinks
amit429 Feb 5, 2026
3534acc
fix the redirection from app killed state
amit429 Feb 5, 2026
b280e92
testing
amit429 Feb 5, 2026
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
33 changes: 33 additions & 0 deletions __mocks__/@capacitor/app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/* eslint-disable custom-rules/one-interface-per-file */
Copy link
Contributor Author

Choose a reason for hiding this comment

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

/* eslint-disable custom-rules/prefer-semantic-extension-name */
export interface AppState {
isActive: boolean;
}

// Minimal shape of AppInfo used by the app.
export interface AppInfo {
version: string;
}

// Use a loose event shape so this mock is compatible with both
// 'appStateChange' (which has isActive) and 'appUrlOpen' (which has url),
// avoiding type errors in application code during tests.
export type AppListener = (event: { [key: string]: any }) => void | Promise<void>;

Check failure on line 15 in __mocks__/@capacitor/app.ts

View workflow job for this annotation

GitHub Actions / Run linters

Unexpected any. Specify a different type

export const App = {
addListener(_eventName: string, _listener: AppListener): Promise<{ remove: () => void }> {

Check failure on line 18 in __mocks__/@capacitor/app.ts

View workflow job for this annotation

GitHub Actions / Run linters

'_listener' is defined but never used

Check failure on line 18 in __mocks__/@capacitor/app.ts

View workflow job for this annotation

GitHub Actions / Run linters

'_eventName' is defined but never used
// In tests we don't actually hook into any real app lifecycle,
// so just return a no-op remover.
return Promise.resolve({ remove: () => {} });

Check failure on line 21 in __mocks__/@capacitor/app.ts

View workflow job for this annotation

GitHub Actions / Run linters

Unexpected empty method 'remove'
},
Comment on lines +12 to +22
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Fix the lint blockers in the App mock, boss.
CI is already red on explicit any, unused params, and the empty remove body. Tighten the type and mark params used so the show goes on.

🛠️ Suggested fix
-export type AppListener = (event: { [key: string]: any }) => void | Promise<void>;
+export type AppListener = (event: Record<string, unknown>) => void | Promise<void>;

 export const App = {
   addListener(_eventName: string, _listener: AppListener): Promise<{ remove: () => void }> {
+    void _eventName;
+    void _listener;
     // In tests we don't actually hook into any real app lifecycle,
     // so just return a no-op remover.
-    return Promise.resolve({ remove: () => {} });
+    return Promise.resolve({ remove: () => { return; } });
   },
🧰 Tools
🪛 GitHub Actions: Lint

[error] 15-15: Unexpected any. Specify a different type @typescript-eslint/no-explicit-any

🪛 GitHub Check: Run linters

[failure] 21-21:
Unexpected empty method 'remove'


[failure] 18-18:
'_listener' is defined but never used


[failure] 18-18:
'_eventName' is defined but never used


[failure] 15-15:
Unexpected any. Specify a different type

🤖 Prompt for AI Agents
In `@__mocks__/`@capacitor/app.ts around lines 12 - 22, Replace the loose any and
the lint-blocking unused/empty functions in the App mock: change AppListener
from (event: { [key: string]: any }) to a stricter shape like (event:
Record<string, unknown>) => void | Promise<void>, update addListener signature
to use concrete param names (e.g., eventName: string, listener: AppListener),
reference those params inside the function (e.g., void eventName; void
listener;) to avoid unused-param errors, and return a remover whose body is
non-empty but trivial (e.g., () => void 0) so eslint doesn't flag an empty
function; keep the Promise.resolve return and the exported App object as-is.


// Simple stub for App.getInfo used by DeviceService.
getInfo(): Promise<AppInfo> {
return Promise.resolve({ version: '1.2.3' });
},

exitApp(): Promise<void> {
return Promise.resolve();
},
};

39 changes: 39 additions & 0 deletions __mocks__/@capacitor/push-notifications.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// The interfaces are still maintained in the same file to make the tests pass
export interface PermissionStatus {

Check failure on line 2 in __mocks__/@capacitor/push-notifications.ts

View workflow job for this annotation

GitHub Actions / Run linters

Interfaces should be defined only in files with .model.ts extension
receive: string;
}

export interface Token {

Check failure on line 6 in __mocks__/@capacitor/push-notifications.ts

View workflow job for this annotation

GitHub Actions / Run linters

Interfaces should be defined only in files with .model.ts extension

Check failure on line 6 in __mocks__/@capacitor/push-notifications.ts

View workflow job for this annotation

GitHub Actions / Run linters

Only one TypeScript interface or type is allowed per file
value?: string;
}

export interface PushNotificationEvent {

Check failure on line 10 in __mocks__/@capacitor/push-notifications.ts

View workflow job for this annotation

GitHub Actions / Run linters

Interfaces should be defined only in files with .model.ts extension

Check failure on line 10 in __mocks__/@capacitor/push-notifications.ts

View workflow job for this annotation

GitHub Actions / Run linters

Only one TypeScript interface or type is allowed per file
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[key: string]: any;
}

export type PushNotificationListener = (event: PushNotificationEvent) => void;

Check failure on line 15 in __mocks__/@capacitor/push-notifications.ts

View workflow job for this annotation

GitHub Actions / Run linters

Only one TypeScript interface or type is allowed per file

Comment on lines 2 to 16
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Boss, lint police will halt the show — move type declarations into .model.ts (one per file).

CI already fails because this mock defines multiple interfaces/types in a non-.model.ts file. Split each type into its own .model.ts file and re-export here. While moving, swap any for unknown/Record<string, unknown> to satisfy no-explicit-any.

🛠️ Suggested change (re-export types from model files)
-export interface PermissionStatus {
-  receive: string;
-}
-
-export interface Token {
-  value?: string;
-  // Allow additional properties so the type is flexible in tests.
-  [key: string]: any;
-}
-
-export interface PushNotificationEvent {
-  notification?: {
-    data?: any;
-  };
-}
-
-export type PushNotificationListener = (event: PushNotificationEvent) => void;
+export type { PermissionStatus } from './permission-status.model';
+export type { Token } from './token.model';
+export type { PushNotificationEvent } from './push-notification-event.model';
+export type { PushNotificationListener } from './push-notification-listener.model';

Example model file:

// __mocks__/@capacitor/token.model.ts
export type Token = { value?: string; [key: string]: unknown };
🧰 Tools
🪛 GitHub Actions: Lint

[error] 1-1: ESLint: Interfaces should be defined only in files with .model.ts extension (custom-rules/prefer-semantic-extension-name)

🪛 GitHub Check: Run linters

[failure] 17-17:
Types should be defined only in files with .model.ts extension


[failure] 17-17:
Only one TypeScript interface or type is allowed per file


[failure] 13-13:
Unexpected any. Specify a different type


[failure] 11-11:
Interfaces should be defined only in files with .model.ts extension


[failure] 11-11:
Only one TypeScript interface or type is allowed per file


[failure] 8-8:
Unexpected any. Specify a different type


[failure] 5-5:
Interfaces should be defined only in files with .model.ts extension


[failure] 5-5:
Only one TypeScript interface or type is allowed per file


[failure] 1-1:
Interfaces should be defined only in files with .model.ts extension

🤖 Prompt for AI Agents
In `@__mocks__/`@capacitor/push-notifications.ts around lines 1 - 18, Split each
type/interface into its own model file and re-export them from this mock: create
PermissionStatus.model.ts exporting PermissionStatus, Token.model.ts exporting
Token (change any to unknown or Record<string, unknown> for the index signature
and notification data), PushNotificationEvent.model.ts exporting
PushNotificationEvent (use unknown/Record<string, unknown> instead of any), and
PushNotificationListener.model.ts exporting PushNotificationListener; then
update this mock file to import and re-export those symbols (PermissionStatus,
Token, PushNotificationEvent, PushNotificationListener) so the mock contains no
type declarations and no use of the any type.

export const PushNotifications = {
requestPermissions(): Promise<PermissionStatus> {
return Promise.resolve({ receive: 'denied' });
},

checkPermissions(): Promise<PermissionStatus> {
return Promise.resolve({ receive: 'denied' });
},

register(): Promise<void> {
return Promise.resolve();
},

unregister(): Promise<void> {
return Promise.resolve();
},

// eslint-disable-next-line @typescript-eslint/no-unused-vars
addListener(_eventName: string, _listener: any): Promise<{ remove: () => void }> {
return Promise.resolve({ remove: () => {} });
},
};

5 changes: 5 additions & 0 deletions src/app/app.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { NavController } from '@ionic/angular/standalone';
import { UserEventService } from './core/services/user-event.service';
import { DeviceService } from './core/services/device.service';
import { GmapsService } from './core/services/gmaps.service';
import { PushNotificationService } from './core/services/push-notification.service';

import { SplashScreen } from '@capacitor/splash-screen';
import { StatusBar } from '@capacitor/status-bar';
Expand Down Expand Up @@ -73,6 +74,7 @@ describe('AppComponent', () => {
let gmapsService: jasmine.SpyObj<GmapsService>;
let menuController: jasmine.SpyObj<MenuController>;
let backButtonService: jasmine.SpyObj<BackButtonService>;
let pushNotificationService: jasmine.SpyObj<PushNotificationService>;
beforeEach(waitForAsync(() => {
// Mock Capacitor plugins before component creation
// Create spies that return resolved promises
Expand Down Expand Up @@ -155,6 +157,7 @@ describe('AppComponent', () => {
const gmapsServiceSpy = jasmine.createSpyObj('GmapsService', ['loadLibrary']);
const menuControllerSpy = jasmine.createSpyObj('MenuController', ['swipeGesture']);
const backButtonServiceSpy = jasmine.createSpyObj('BackButtonService', ['showAppCloseAlert']);
const pushNotificationServiceSpy = jasmine.createSpyObj('PushNotificationService', ['unregister']);

// Configure tracking service with missing methods
trackingServiceSpy.updateIdentityIfNotPresent = jasmine.createSpy('updateIdentityIfNotPresent').and.resolveTo();
Expand Down Expand Up @@ -190,6 +193,7 @@ describe('AppComponent', () => {
{ provide: GmapsService, useValue: gmapsServiceSpy },
{ provide: MenuController, useValue: menuControllerSpy },
{ provide: BackButtonService, useValue: backButtonServiceSpy },
{ provide: PushNotificationService, useValue: pushNotificationServiceSpy },
],
})
.overrideComponent(AppComponent, {
Expand Down Expand Up @@ -220,6 +224,7 @@ describe('AppComponent', () => {
gmapsService = TestBed.inject(GmapsService) as jasmine.SpyObj<GmapsService>;
menuController = TestBed.inject(MenuController) as jasmine.SpyObj<MenuController>;
backButtonService = TestBed.inject(BackButtonService) as jasmine.SpyObj<BackButtonService>;
pushNotificationService = TestBed.inject(PushNotificationService) as jasmine.SpyObj<PushNotificationService>;
}));

describe('ngAfterViewInit', () => {
Expand Down
7 changes: 6 additions & 1 deletion src/app/app.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ import { NgClass } from '@angular/common';
import { FyConnectionComponent } from './shared/components/fy-connection/fy-connection.component';
import { Capacitor } from '@capacitor/core';
import { AppShortcuts } from '@capawesome/capacitor-app-shortcuts';
import { PushNotificationService } from './core/services/push-notification.service';
import { registerPlugin } from '@capacitor/core';
import { PushNotifications } from '@capacitor/push-notifications';

@Component({
selector: 'app-root',
Expand All @@ -53,6 +56,8 @@ export class AppComponent implements OnInit, AfterViewInit {

private router = inject(Router);

private pushNotificationService = inject(PushNotificationService);

private activatedRoute = inject(ActivatedRoute);

private userEventService = inject(UserEventService);
Expand Down Expand Up @@ -242,7 +247,6 @@ export class AppComponent implements OnInit, AfterViewInit {
this.footerService.selectionMode$.subscribe((isEnabled) => {
this.showFooter = !isEnabled;
});

// This was done as a security fix for appknox
// eslint-disable-next-line
if ((window as any) && (window as any).localStorage) {
Expand Down Expand Up @@ -293,6 +297,7 @@ export class AppComponent implements OnInit, AfterViewInit {
this.userEventService.onLogout(() => {
this.trackingService.onSignOut();
this.freshChatService.destroy();
this.pushNotificationService.unregister();
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Unregisters a token on log out

this.isSwitchedToDelegator = false;
Comment on lines 301 to 305
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Handle unregister promise (and avoid double-unregister).
unregister() returns a Promise; unhandled rejection can surface, and the service already auto-unregisters on logout. Pick one place, and if you keep this call, guard it.

🐛 Suggested fix (keep call, guard it)
-      this.pushNotificationService.unregister();
+      void this.pushNotificationService.unregister().catch(noop);
🤖 Prompt for AI Agents
In `@src/app/app.component.ts` around lines 296 - 300, The logout handler
registered with userEventService.onLogout calls
pushNotificationService.unregister() but doesn't handle its returned Promise and
may double-unregister; update the onLogout callback (in the block where
trackingService.onSignOut(), freshChatService.destroy(),
pushNotificationService.unregister(), isSwitchedToDelegator = false are called)
to guard and handle the promise: first check a registration state (e.g.
pushNotificationService.isRegistered or an internal flag) before calling
unregister, and if keeping the call always, ensure you handle the Promise by
awaiting it or attaching .catch(...) to swallow or log harmless "already
unregistered" errors to avoid unhandled rejections and duplicate unregister
attempts.

this.router.navigate(['/', 'auth', 'sign_in']);
});
Expand Down
5 changes: 5 additions & 0 deletions src/app/core/mock-data/org-settings.data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,11 @@ export const orgSettingsRes: OrgSettings = deepFreeze({
enabled: true,
virtual_card_settings_enabled: true,
},
mobile_notification_settings: {
allowed: true,
enabled: true,
unsubscribed_events: [],
},
regional_settings: {
allowed: true,
enabled: true,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface MobilePushNotificationSettings {
enabled: boolean;
allowed: boolean;
unsubscribed_events: string[];
}
3 changes: 3 additions & 0 deletions src/app/core/models/org-settings.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { TaxGroup } from './tax-group.model';
import { TransactionMandatoryFields } from './transaction-mandatory-fields.model';
import { User } from './user.model';
import { OrgUserMandatoryFields } from './org-user-mandatory-fields.model';
import { MobilePushNotificationSettings } from './mobile-push-notification-settings.model';

export enum EmailEvents {
DELEGATOR_SUBSCRIPTION = 'DELEGATOR_SUBSCRIPTION',
Expand Down Expand Up @@ -478,6 +479,7 @@ export interface OrgSettingsResponse {
is_new_critical_policy_violation_flow_enabled?: boolean;
regional_settings?: RegionalSettings;
auto_report_approval_settings?: AutoReportApprovalSettings;
mobile_notification_settings?: MobilePushNotificationSettings;
}

export interface UiPolicySettings {
Expand Down Expand Up @@ -600,4 +602,5 @@ export interface OrgSettings {
is_new_critical_policy_violation_flow_enabled?: boolean;
regional_settings?: RegionalSettings;
auto_report_approval_settings?: AutoReportApprovalSettings;
mobile_notification_settings?: MobilePushNotificationSettings;
}
18 changes: 18 additions & 0 deletions src/app/core/services/org-user.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,24 @@ describe('OrgUserService', () => {
});
});

it('sendDeviceToken(): should fetch existing tokens and append new token when sending to backend', (done) => {
const token = 'test-device-token';
const existingTokens: string[] = ['existing-token-1'];
const expectedTokens = [...existingTokens, token];

spenderPlatformV1ApiService.get.and.returnValue(of({ data: { tokens: existingTokens } } as any));
spenderPlatformV1ApiService.post.and.returnValue(of({}));

orgUserService.sendDeviceToken(token).subscribe((res) => {
expect(spenderPlatformV1ApiService.get).toHaveBeenCalledWith('/device_token');
expect(spenderPlatformV1ApiService.post).toHaveBeenCalledWith('/device_token', {
data: { tokens: expectedTokens },
});
expect(res).toEqual({});
done();
});
});
Comment on lines 188 to 204
Copy link

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

One test case? Naanga oru dharavai commit pannaa, noorudhavai test pannuven!

The test covers the happy path like a hero's entry scene, but Superstar needs to see some villain-fighting scenarios too:

  1. Empty tokens array: What happens when no existing tokens? Backend could return { data: { tokens: [] } } or { data: {} }
  2. Duplicate token handling: If the same token already exists in the array (if deduplication is added per previous comment)

Also, Line 192 uses as any - like fighting without knowing the enemy. Consider using proper type:

spenderPlatformV1ApiService.get.and.returnValue(
  of({ data: { tokens: existingTokens } } as PlatformApiResponse<{ tokens: string[] }>)
);
🎬 Additional test cases - Superstar collection
it('sendDeviceToken(): should handle empty existing tokens', (done) => {
  const token = 'new-device-token';

  spenderPlatformV1ApiService.get.and.returnValue(
    of({ data: { tokens: [] } } as PlatformApiResponse<{ tokens: string[] }>)
  );
  spenderPlatformV1ApiService.post.and.returnValue(of({}));

  orgUserService.sendDeviceToken(token).subscribe((res) => {
    expect(spenderPlatformV1ApiService.post).toHaveBeenCalledWith('/users/device_token', {
      data: { tokens: [token] },
    });
    done();
  });
});

it('sendDeviceToken(): should handle null tokens from backend', (done) => {
  const token = 'new-device-token';

  spenderPlatformV1ApiService.get.and.returnValue(
    of({ data: { tokens: null } } as any)
  );
  spenderPlatformV1ApiService.post.and.returnValue(of({}));

  orgUserService.sendDeviceToken(token).subscribe((res) => {
    expect(spenderPlatformV1ApiService.post).toHaveBeenCalledWith('/users/device_token', {
      data: { tokens: [token] },
    });
    done();
  });
});

Based on learnings: "In the Fyle mobile app, the migration from OrgUserSettingsService to PlatformEmployeeSettingsService introduced a potential breaking change where mileage_settings could be null or undefined... indicating this is a real-world scenario that needs defensive handling."

🤖 Prompt for AI Agents
In `@src/app/core/services/org-user.service.spec.ts` around lines 188 - 203, Add
unit tests for edge cases in orgUserService.sendDeviceToken: cover backend
returning empty tokens array, data.tokens being null/undefined, and the
duplicate-token case if deduplication is or will be implemented; update the test
stubs to use the proper response type (use PlatformApiResponse<{ tokens:
string[] }> for spenderPlatformV1ApiService.get returnValue instead of as any)
and assert that spenderPlatformV1ApiService.post is called with the expected
tokens payload (e.g., [token] when no existing tokens, or existingTokens when
duplicate handling prevents adding the same token).


it('should return false if the user is not switched to a delegator', (done) => {
jwtHelperService.decodeToken.and.returnValue(accessTokenData);
// This token contains the user details such as user id, org id, org user id, roles, scopes, etc.
Expand Down
21 changes: 21 additions & 0 deletions src/app/core/services/org-user.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,27 @@ export class OrgUserService {
);
}

getDeviceTokens(): Observable<string[]> {
return this.spenderPlatformV1ApiService
.get<PlatformApiResponse<{ tokens: string[] }>>('/device_token')
.pipe(map((response) => response.data?.tokens ?? []));
}

sendDeviceToken(token: string): Observable<unknown> {
return this.getDeviceTokens().pipe(
map((existingTokens) => {
const tokens = existingTokens ?? [];
tokens.push(token);
return tokens;
}),
switchMap((tokens) =>
this.spenderPlatformV1ApiService.post('/device_token', {
data: { tokens },
}),
),
);
}
Comment on lines 110 to 130
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Search for device_token and /users/device_token references
rg -n -C 3 "/users/device_token|device_token" --type=ts

Repository: fylein/fyle-mobile-app

Length of output: 1196


🏁 Script executed:

# Look for any API documentation or interface definitions related to device tokens
rg -n "tokens|deviceToken" --type=ts -A 2 -B 2 | head -100

Repository: fylein/fyle-mobile-app

Length of output: 372


🏁 Script executed:

# Check for any test files that might show expected payload
fd -e spec.ts -e test.ts | xargs rg -l "device_token\|deviceToken" 2>/dev/null

Repository: fylein/fyle-mobile-app

Length of output: 50


🏁 Script executed:

# Look for any backend API contract files or documentation
fd -e "*.json" -e "*.md" -e "*.yaml" -e "*.yml" | xargs rg -l "device_token" 2>/dev/null

Repository: fylein/fyle-mobile-app

Length of output: 154


🏁 Script executed:

cat -n src/app/core/services/org-user.service.spec.ts | sed -n '185,200p'

Repository: fylein/fyle-mobile-app

Length of output: 624


Fix the payload schema to match the expected contract.

The implementation sends { data: { tokens: [token] } } but the test expects { data: { token } }. Device token registration is broken and the test will fail.

Required fix
  const payload = {
    data: {
-     tokens: [token],
+     token,
    },
  };
📝 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
sendDeviceToken(token: string): Observable<unknown> {
const payload = {
data: {
tokens: [token],
},
};
return this.spenderPlatformV1ApiService.post('/users/device_token', payload);
}
sendDeviceToken(token: string): Observable<unknown> {
const payload = {
data: {
token,
},
};
return this.spenderPlatformV1ApiService.post('/users/device_token', payload);
}
🤖 Prompt for AI Agents
In `@src/app/core/services/org-user.service.ts` around lines 104 - 112, The
sendDeviceToken implementation builds the wrong payload shape (it sends
data.tokens as an array); update sendDeviceToken to send data.token (a single
token string) to match the contract and tests. In the sendDeviceToken method
change the payload construction so payload is { data: { token } } and continue
to call spenderPlatformV1ApiService.post('/users/device_token', payload) as
before.

Comment on lines 110 to 130
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Mind it! Duplicate tokens can sneak in like a villain!

Thalaiva sees two concerns here:

  1. Line 113: The existingTokens ?? [] check is unnecessary - getDeviceTokens() already returns [] as default. Like bringing coconut to Kerala!

  2. No deduplication: If the same device token is sent twice, it gets added twice to the array. This could bloat the token list over time and cause unnecessary notifications.

🎬 Proposed fix - Superstar style
  sendDeviceToken(token: string): Observable<unknown> {
    return this.getDeviceTokens().pipe(
      map((existingTokens) => {
-       const tokens = existingTokens ?? [];
-       tokens.push(token);
-       return tokens;
+       if (existingTokens.includes(token)) {
+         return existingTokens;
+       }
+       return [...existingTokens, token];
      }),
      switchMap((tokens) =>
        this.spenderPlatformV1ApiService.post('/users/device_token', {
          data: { tokens },
        }),
      ),
    );
  }
🤖 Prompt for AI Agents
In `@src/app/core/services/org-user.service.ts` around lines 110 - 123, In
sendDeviceToken, remove the unnecessary existingTokens ?? [] fallback
(getDeviceTokens already returns an array) and ensure you deduplicate before
posting: obtain tokens from getDeviceTokens(), add the new token only if it's
not already present (or build a unique array via a Set) so duplicates aren't
sent, then call spenderPlatformV1ApiService.post('/users/device_token', { data:
{ tokens } }); reference sendDeviceToken and getDeviceTokens when making the
change.


getUserById(userId: string): Observable<EouApiResponse> {
return this.apiService.get('/eous/' + userId);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,11 @@ export class PlatformOrgSettingsService {
enabled: incoming.auto_report_approval_settings?.enabled,
amount_threshold: incoming.auto_report_approval_settings?.amount_threshold,
},
mobile_notification_settings: {
allowed: incoming.mobile_notification_settings && incoming.mobile_notification_settings.allowed,
enabled: incoming.mobile_notification_settings && incoming.mobile_notification_settings.enabled,
unsubscribed_events: incoming.mobile_notification_settings && incoming.mobile_notification_settings.unsubscribed_events,
},
Comment on lines +424 to +428
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Boss, avoid truthy-but-empty mobile_notification_settings.
When incoming is missing, you still create an object with undefined fields; downstream if (mobile_notification_settings) becomes true and array ops can crash. Guard and default safely.

🔥 Suggested fix
-      mobile_notification_settings: {
-        allowed: incoming.mobile_notification_settings && incoming.mobile_notification_settings.allowed,
-        enabled: incoming.mobile_notification_settings && incoming.mobile_notification_settings.enabled,
-        unsubscribed_events: incoming.mobile_notification_settings && incoming.mobile_notification_settings.unsubscribed_events,
-      },
+      mobile_notification_settings: incoming.mobile_notification_settings
+        ? {
+            allowed: incoming.mobile_notification_settings.allowed ?? false,
+            enabled: incoming.mobile_notification_settings.enabled ?? false,
+            unsubscribed_events: incoming.mobile_notification_settings.unsubscribed_events ?? [],
+          }
+        : undefined,
📝 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
mobile_notification_settings: {
allowed: incoming.mobile_notification_settings && incoming.mobile_notification_settings.allowed,
enabled: incoming.mobile_notification_settings && incoming.mobile_notification_settings.enabled,
unsubscribed_events: incoming.mobile_notification_settings && incoming.mobile_notification_settings.unsubscribed_events,
},
mobile_notification_settings: incoming.mobile_notification_settings
? {
allowed: incoming.mobile_notification_settings.allowed ?? false,
enabled: incoming.mobile_notification_settings.enabled ?? false,
unsubscribed_events: incoming.mobile_notification_settings.unsubscribed_events ?? [],
}
: undefined,
🤖 Prompt for AI Agents
In `@src/app/core/services/platform/v1/spender/org-settings.service.ts` around
lines 424 - 428, The current mapping always creates a truthy
mobile_notification_settings object with undefined fields from incoming, which
breaks downstream checks; change the assignment so mobile_notification_settings
is either undefined when incoming.mobile_notification_settings is falsy or a
fully-defaulted object when present: i.e., use
incoming.mobile_notification_settings ? a safe/defaulted object (populate
allowed, enabled with booleans and unsubscribed_events with an empty array) :
undefined; update the mapping that constructs mobile_notification_settings to
use this conditional and reference incoming and mobile_notification_settings
accordingly.

};

Object.keys(orgSettings).forEach((settingsType) => {
Expand Down
Loading
Loading