Skip to content

Commit a559b55

Browse files
committed
refactor: update ModalFragment to use newInstance factory method for clientId handling
- Replaced direct constructor calls to ModalFragment with newInstance method across tests and implementation files. - Added tests to ensure proper state restoration and argument handling for ModalFragment. - Updated ModalFragment class to include a companion object for argument management, ensuring compatibility with Android's fragment lifecycle.
1 parent 9ea1c89 commit a559b55

File tree

7 files changed

+181
-11
lines changed

7 files changed

+181
-11
lines changed

library/src/androidTest/java/com/paypal/messages/ModalExternalLinkTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ class ModalExternalLinkTest {
3737
val webView = WebView(recordingContext)
3838

3939
// Initialize the modal WebView configuration
40-
val fragment = ModalFragment(clientId = "test-client-id")
40+
val fragment = ModalFragment.newInstance("test-client-id")
4141
fragment.setupWebView(webView)
4242

4343
// Load a minimal page that triggers a target=_blank navigation
Lines changed: 149 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,159 @@
1-
package com.paypal.messages.totest
1+
package com.paypal.messages
22

3+
import android.os.Bundle
4+
import androidx.fragment.app.testing.launchFragmentInContainer
35
import androidx.test.ext.junit.runners.AndroidJUnit4
6+
import org.junit.Assert.assertEquals
7+
import org.junit.Assert.assertNotNull
48
import org.junit.Test
59
import org.junit.runner.RunWith
610

711
@RunWith(AndroidJUnit4::class)
812
class ModalFragmentTest {
13+
14+
@Test
15+
fun newInstance_setsClientIdInArguments() {
16+
val testClientId = "test-client-id-123"
17+
val fragment = ModalFragment.newInstance(testClientId)
18+
19+
assertNotNull("Fragment should not be null", fragment)
20+
assertNotNull("Arguments should not be null", fragment.arguments)
21+
assertEquals(
22+
"ClientId should be stored in arguments",
23+
testClientId,
24+
fragment.arguments?.getString("clientId"),
25+
)
26+
}
27+
28+
@Test
29+
fun fragmentRestoration_preservesClientId() {
30+
val testClientId = "test-client-id-restoration"
31+
32+
// Create fragment using factory method
33+
val originalFragment = ModalFragment.newInstance(testClientId)
34+
35+
// Get the arguments that would be saved (Android automatically saves/restores arguments)
36+
val savedArguments = originalFragment.arguments
37+
38+
// Create a new fragment instance (simulating Android's recreation when "Don't keep activities" is enabled)
39+
// This uses the no-arg constructor, which is required for proper state restoration
40+
val recreatedFragment = ModalFragment()
41+
42+
// Restore the arguments (Android does this automatically during recreation)
43+
recreatedFragment.arguments = savedArguments
44+
45+
// Verify clientId can be accessed (this would fail with the old constructor approach)
46+
// We can't directly access private clientId, but we can verify arguments are set
47+
assertNotNull("Recreated fragment arguments should not be null", recreatedFragment.arguments)
48+
assertEquals(
49+
"ClientId should be preserved in recreated fragment",
50+
testClientId,
51+
recreatedFragment.arguments?.getString("clientId"),
52+
)
53+
}
54+
55+
@Test
56+
fun fragmentRecreation_withActivityScenario() {
57+
val testClientId = "test-client-id-scenario"
58+
59+
// Launch fragment in a container using FragmentScenario
60+
val scenario = launchFragmentInContainer<ModalFragment>(
61+
Bundle().apply {
62+
putString("clientId", testClientId)
63+
},
64+
)
65+
66+
// Verify fragment is created
67+
scenario.onFragment { fragment ->
68+
assertNotNull("Fragment should be created", fragment)
69+
assertNotNull("Fragment arguments should be set", fragment.arguments)
70+
assertEquals(
71+
"ClientId should be accessible",
72+
testClientId,
73+
fragment.arguments?.getString("clientId"),
74+
)
75+
}
76+
77+
// Simulate activity recreation (like "Don't keep activities")
78+
scenario.recreate()
79+
80+
// Verify fragment is recreated with same arguments
81+
scenario.onFragment { recreatedFragment ->
82+
assertNotNull("Recreated fragment should not be null", recreatedFragment)
83+
assertNotNull("Recreated fragment arguments should not be null", recreatedFragment.arguments)
84+
assertEquals(
85+
"ClientId should be preserved after recreation",
86+
testClientId,
87+
recreatedFragment.arguments?.getString("clientId"),
88+
)
89+
}
90+
}
91+
92+
@Test
93+
fun fragmentWithoutArguments_throwsException() {
94+
// Create fragment without using newInstance (simulating old way or error case)
95+
val fragment = ModalFragment()
96+
fragment.arguments = null // No arguments set
97+
98+
// Accessing clientId property should throw IllegalStateException
99+
// We test this indirectly by verifying the fragment can't work without arguments
100+
// Since clientId is private, we test via reflection or by ensuring arguments are required
101+
assertNotNull("Fragment should be created", fragment)
102+
// The actual exception would be thrown when clientId is accessed internally
103+
// This test documents the expected behavior
104+
}
105+
106+
@Test
107+
fun newInstance_differentClientIds() {
108+
val clientId1 = "client-id-1"
109+
val clientId2 = "client-id-2"
110+
111+
val fragment1 = ModalFragment.newInstance(clientId1)
112+
val fragment2 = ModalFragment.newInstance(clientId2)
113+
114+
assertEquals(
115+
"First fragment should have first clientId",
116+
clientId1,
117+
fragment1.arguments?.getString("clientId"),
118+
)
119+
assertEquals(
120+
"Second fragment should have second clientId",
121+
clientId2,
122+
fragment2.arguments?.getString("clientId"),
123+
)
124+
}
125+
9126
@Test
10-
fun testSomething() {
11-
//
127+
fun fragmentStateRestoration_simulatesDontKeepActivities() {
128+
val testClientId = "test-client-dont-keep-activities"
129+
130+
// Step 1: Create fragment using factory method (normal flow)
131+
val fragment = ModalFragment.newInstance(testClientId)
132+
133+
// Step 2: Simulate what happens when "Don't keep activities" is enabled:
134+
// - Activity is destroyed
135+
// - Fragment arguments are automatically saved by Android
136+
// - Fragment instance is destroyed
137+
val savedArguments = Bundle().apply {
138+
putAll(fragment.arguments ?: Bundle())
139+
}
140+
141+
// Step 3: Simulate Android recreating the fragment (uses no-arg constructor)
142+
// This is the key fix: the fragment must have a no-arg constructor
143+
val recreatedFragment = ModalFragment()
144+
145+
// Step 4: Android restores the arguments Bundle automatically
146+
recreatedFragment.arguments = savedArguments
147+
148+
// Step 5: Verify the fragment can function properly
149+
// The key test: can we access clientId without constructor?
150+
assertEquals(
151+
"Recreated fragment should have same clientId",
152+
testClientId,
153+
recreatedFragment.arguments?.getString("clientId"),
154+
)
155+
156+
// This test verifies the fix: fragment can be recreated using no-arg constructor
157+
// and clientId is accessible via arguments Bundle
12158
}
13159
}

library/src/androidTest/java/com/paypal/messages/PayPalMessageViewTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ class PayPalMessageViewTest {
113113
// fun dismissAfterFragmentDetached_shouldThrow() {
114114
// val scenario: ActivityScenario<TestActivity> = ActivityScenario.launch(TestActivity::class.java)
115115
// scenario.onActivity { activity: TestActivity ->
116-
// val fragment = ModalFragment("test_client_id")
116+
// val fragment = ModalFragment.newInstance("test_client_id")
117117
// fragment.show(activity.supportFragmentManager, "test")
118118
//
119119
// // Remove the fragment to simulate detachment

library/src/main/java/com/paypal/messages/ModalFragment.kt

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,13 +50,35 @@ import java.util.UUID
5050
import kotlin.system.measureTimeMillis
5151
import com.paypal.messages.config.PayPalMessageOfferType as OfferType
5252

53-
internal class ModalFragment(
54-
private val clientId: String,
55-
) : BottomSheetDialogFragment() {
53+
internal class ModalFragment : BottomSheetDialogFragment() {
54+
companion object {
55+
private const val ARG_CLIENT_ID = "clientId"
56+
57+
/**
58+
* Factory method to create a new instance of ModalFragment with clientId.
59+
* This pattern is required for proper fragment state restoration when
60+
* "Don't keep activities" is enabled.
61+
*/
62+
fun newInstance(clientId: String): ModalFragment {
63+
return ModalFragment().apply {
64+
arguments = Bundle().apply {
65+
putString(ARG_CLIENT_ID, clientId)
66+
}
67+
}
68+
}
69+
}
70+
5671
private val TAG = "PayPalMessageModal"
5772
private val offsetTop = 50.dp
5873
private val gson = GsonBuilder().setPrettyPrinting().create()
5974

75+
/**
76+
* Gets the clientId from arguments Bundle.
77+
* This ensures proper state restoration when Android recreates the fragment.
78+
*/
79+
private val clientId: String
80+
get() = arguments?.getString(ARG_CLIENT_ID) ?: throw IllegalStateException("clientId must be set via newInstance()")
81+
6082
private var modalUrl: String? = null
6183

6284
// MODAL CONFIG VALUES

library/src/main/java/com/paypal/messages/PayPalComposableModal.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,9 @@ fun PayPalComposableModal(
5959
var errorMessage by remember { mutableStateOf("") }
6060

6161
// Create the ModalFragment instance for WebView setup
62-
val modalFragment = remember { ModalFragment(clientId) }
62+
// Use clientId as key to ensure fragment is recreated if clientId changes
63+
// This also ensures proper recreation when "Don't keep activities" is enabled
64+
val modalFragment = remember(clientId) { ModalFragment.newInstance(clientId) }
6365
val offerEnum = offerType?.let {
6466
try {
6567
PayPalMessageOfferType.valueOf(it)

library/src/main/java/com/paypal/messages/PayPalModalActivity.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -368,7 +368,7 @@ class PayPalModalActivity : ComponentActivity() {
368368
}
369369

370370
// Create and setup the modal fragment
371-
val modalFragment = ModalFragment(clientId)
371+
val modalFragment = ModalFragment.newInstance(clientId)
372372
val offerEnum = offerType?.let {
373373
try {
374374
com.paypal.messages.config.PayPalMessageOfferType.valueOf(it)

library/src/main/java/com/paypal/messages/data/PayPalMessageDataProvider.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -311,7 +311,7 @@ class PayPalMessageDataProvider {
311311
// For AppCompatActivity contexts, use ModalFragment
312312
appCompatContext != null -> {
313313
val modal = modalInstances[instanceId] ?: run {
314-
val newModal = ModalFragment(config.data.clientID)
314+
val newModal = ModalFragment.newInstance(config.data.clientID)
315315

316316
// Build modal config
317317
val modalConfig = ModalConfig(

0 commit comments

Comments
 (0)