Skip to content

Commit d7bffcd

Browse files
committed
fix(voip): localize phone-state OK button; gate FCM before busy check
Slice 1: Add Ok i18n key and use i18n.t for READ_PHONE_STATE rationale button. Slice 2: Evaluate incoming push lifetime/expiry before hasActiveCall so stale pushes do not call rejectBusyCall or showIncomingCall; extract pure dispatch helper with JUnit tests; add junit test dependency. Made-with: Cursor
1 parent ab85a8f commit d7bffcd

7 files changed

Lines changed: 89 additions & 7 deletions

File tree

android/app/build.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,8 @@ dependencies {
153153

154154
// For SecureKeystore (EncryptedSharedPreferences)
155155
implementation 'androidx.security:security-crypto:1.1.0'
156+
157+
testImplementation 'junit:junit:4.13.2'
156158
}
157159

158160
apply plugin: 'com.google.gms.google-services'
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package chat.rocket.reactnative.voip
2+
3+
/**
4+
* Pure routing for an incoming VoIP FCM push after [VoipPayload.isVoipIncomingCall] is true.
5+
* Stale (invalid or expired lifetime) pushes must not reach busy vs show branching.
6+
*/
7+
internal enum class VoipIncomingPushAction {
8+
STALE,
9+
REJECT_BUSY,
10+
SHOW_INCOMING
11+
}
12+
13+
internal fun decideIncomingVoipPushAction(
14+
isValidForIncomingHandling: Boolean,
15+
hasActiveCall: Boolean
16+
): VoipIncomingPushAction {
17+
if (!isValidForIncomingHandling) {
18+
return VoipIncomingPushAction.STALE
19+
}
20+
return if (hasActiveCall) {
21+
VoipIncomingPushAction.REJECT_BUSY
22+
} else {
23+
VoipIncomingPushAction.SHOW_INCOMING
24+
}
25+
}

android/app/src/main/java/chat/rocket/reactnative/voip/VoipNotification.kt

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -682,10 +682,21 @@ class VoipNotification(private val context: Context) {
682682
fun onMessageReceived(voipPayload: VoipPayload) {
683683
when {
684684
voipPayload.isVoipIncomingCall() -> {
685-
if (hasActiveCall(context)) {
686-
rejectBusyCall(context, voipPayload)
687-
} else {
688-
showIncomingCall(voipPayload)
685+
val isValidForIncoming =
686+
voipPayload.getRemainingLifetimeMs() != null && !voipPayload.isExpired()
687+
when (decideIncomingVoipPushAction(isValidForIncoming, hasActiveCall(context))) {
688+
VoipIncomingPushAction.STALE -> {
689+
if (voipPayload.getRemainingLifetimeMs() == null) {
690+
Log.w(
691+
TAG,
692+
"Skipping incoming VoIP call without a valid createdAt timestamp - callId: ${voipPayload.callId}"
693+
)
694+
} else {
695+
Log.d(TAG, "Skipping expired incoming VoIP call - callId: ${voipPayload.callId}")
696+
}
697+
}
698+
VoipIncomingPushAction.REJECT_BUSY -> rejectBusyCall(context, voipPayload)
699+
VoipIncomingPushAction.SHOW_INCOMING -> showIncomingCall(voipPayload)
689700
}
690701
}
691702
else -> Log.w(TAG, "Ignoring unsupported VoIP payload type: ${voipPayload.type}")
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package chat.rocket.reactnative.voip
2+
3+
import org.junit.Assert.assertEquals
4+
import org.junit.Test
5+
6+
class VoipIncomingCallDispatchTest {
7+
8+
@Test
9+
fun `stale push with active call does not route to reject busy`() {
10+
assertEquals(
11+
VoipIncomingPushAction.STALE,
12+
decideIncomingVoipPushAction(isValidForIncomingHandling = false, hasActiveCall = true)
13+
)
14+
}
15+
16+
@Test
17+
fun `stale push without active call does not route to show incoming`() {
18+
assertEquals(
19+
VoipIncomingPushAction.STALE,
20+
decideIncomingVoipPushAction(isValidForIncomingHandling = false, hasActiveCall = false)
21+
)
22+
}
23+
24+
@Test
25+
fun `valid push with active call rejects busy`() {
26+
assertEquals(
27+
VoipIncomingPushAction.REJECT_BUSY,
28+
decideIncomingVoipPushAction(isValidForIncomingHandling = true, hasActiveCall = true)
29+
)
30+
}
31+
32+
@Test
33+
fun `valid push without active call shows incoming`() {
34+
assertEquals(
35+
VoipIncomingPushAction.SHOW_INCOMING,
36+
decideIncomingVoipPushAction(isValidForIncomingHandling = true, hasActiveCall = false)
37+
)
38+
}
39+
}

app/i18n/locales/en.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -644,6 +644,7 @@
644644
"Open_your_authentication_app_and_enter_the_code": "Open your authentication app and enter the code.",
645645
"OR": "OR",
646646
"OS": "OS",
647+
"Ok": "Ok",
647648
"Overwrites_the_server_configuration_and_use_room_config": "Overwrites the workspace configuration and use room config",
648649
"Owner": "Owner",
649650
"Parent_channel_or_group": "Parent channel or group",

app/lib/methods/voipPhoneStatePermission.test.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,10 @@ describe('requestPhoneStatePermission', () => {
2121

2222
it('requests READ_PHONE_STATE on Android with i18n rationale keys', () => {
2323
jest.resetModules();
24+
const mockT = jest.fn((key: string) => key);
2425
jest.doMock('../../i18n', () => ({
2526
__esModule: true,
26-
default: { t: (key: string) => key }
27+
default: { t: mockT }
2728
}));
2829
jest.doMock('./helpers', () => ({
2930
...jest.requireActual('./helpers'),
@@ -34,6 +35,9 @@ describe('requestPhoneStatePermission', () => {
3435

3536
requestPhoneStatePermission();
3637

38+
expect(mockT).toHaveBeenCalledWith('Ok');
39+
expect(mockT).toHaveBeenCalledWith('Phone_state_permission_message');
40+
expect(mockT).toHaveBeenCalledWith('Phone_state_permission_title');
3741
expect(spy).toHaveBeenCalledWith(PermissionsAndroid.PERMISSIONS.READ_PHONE_STATE, {
3842
buttonPositive: 'Ok',
3943
message: 'Phone_state_permission_message',
@@ -45,7 +49,7 @@ describe('requestPhoneStatePermission', () => {
4549
jest.resetModules();
4650
jest.doMock('../../i18n', () => ({
4751
__esModule: true,
48-
default: { t: (key: string) => key }
52+
default: { t: jest.fn((key: string) => key) }
4953
}));
5054
jest.doMock('./helpers', () => ({
5155
...jest.requireActual('./helpers'),

app/lib/methods/voipPhoneStatePermission.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export const requestPhoneStatePermission = (): void => {
1515
askedThisSession = true;
1616

1717
PermissionsAndroid.request(PermissionsAndroid.PERMISSIONS.READ_PHONE_STATE, {
18-
buttonPositive: 'Ok',
18+
buttonPositive: i18n.t('Ok'),
1919
message: i18n.t('Phone_state_permission_message'),
2020
title: i18n.t('Phone_state_permission_title')
2121
});

0 commit comments

Comments
 (0)