@dynamic-labs-sdk/client 1.8.1 → 1.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/android/src/main/java/xyz/dynamic/client/DynamicClientPackage.kt +10 -0
  2. package/android/src/main/java/xyz/dynamic/client/screenshot/ScreenshotProtectionModule.kt +48 -0
  3. package/dist/{InvalidParamError-DGBih-LJ.cjs → InvalidParamError-C7LLi2cZ.cjs} +91 -13
  4. package/dist/InvalidParamError-C7LLi2cZ.cjs.map +1 -0
  5. package/dist/{InvalidParamError-BKnC9rcJ.esm.js → InvalidParamError-DIPFT3sS.esm.js} +73 -13
  6. package/dist/InvalidParamError-DIPFT3sS.esm.js.map +1 -0
  7. package/dist/{InvalidParamError-Btv8VGSa.native.esm.js → InvalidParamError-YG7yXCq5.native.esm.js} +294 -13
  8. package/dist/InvalidParamError-YG7yXCq5.native.esm.js.map +1 -0
  9. package/dist/NotWaasWalletAccountError-BWrDQFe8.esm.js +18 -0
  10. package/dist/NotWaasWalletAccountError-BWrDQFe8.esm.js.map +1 -0
  11. package/dist/NotWaasWalletAccountError-C8ex1YA_.cjs +23 -0
  12. package/dist/NotWaasWalletAccountError-C8ex1YA_.cjs.map +1 -0
  13. package/dist/NotWaasWalletAccountError-Cd2o0wr0.native.esm.js +18 -0
  14. package/dist/NotWaasWalletAccountError-Cd2o0wr0.native.esm.js.map +1 -0
  15. package/dist/client/core/createCore/getInitialState.d.ts +1 -1
  16. package/dist/client/core/createCore/getInitialState.d.ts.map +1 -1
  17. package/dist/client/createDynamicClient/createDynamicClient.d.ts.map +1 -1
  18. package/dist/client/types/DynamicClient.d.ts +9 -0
  19. package/dist/client/types/DynamicClient.d.ts.map +1 -1
  20. package/dist/core.cjs +24 -5
  21. package/dist/core.cjs.map +1 -1
  22. package/dist/core.esm.js +24 -6
  23. package/dist/core.esm.js.map +1 -1
  24. package/dist/core.native.esm.js +34 -9
  25. package/dist/core.native.esm.js.map +1 -1
  26. package/dist/exports/core.d.ts +4 -0
  27. package/dist/exports/core.d.ts.map +1 -1
  28. package/dist/exports/index.d.ts +0 -7
  29. package/dist/exports/index.d.ts.map +1 -1
  30. package/dist/exports/waasCore.d.ts +2 -0
  31. package/dist/exports/waasCore.d.ts.map +1 -1
  32. package/dist/{getNetworkProviderFromNetworkId-Bm41knUq.native.esm.js → getNetworkProviderFromNetworkId-C46TeXs8.native.esm.js} +17 -12
  33. package/dist/getNetworkProviderFromNetworkId-C46TeXs8.native.esm.js.map +1 -0
  34. package/dist/{getNetworkProviderFromNetworkId-BdgI7g5U.cjs → getNetworkProviderFromNetworkId-CBGWQS_K.cjs} +4 -4
  35. package/dist/{getNetworkProviderFromNetworkId-BdgI7g5U.cjs.map → getNetworkProviderFromNetworkId-CBGWQS_K.cjs.map} +1 -1
  36. package/dist/{getNetworkProviderFromNetworkId-CRuh5pgc.esm.js → getNetworkProviderFromNetworkId-D-wj5mPl.esm.js} +3 -3
  37. package/dist/{getNetworkProviderFromNetworkId-CRuh5pgc.esm.js.map → getNetworkProviderFromNetworkId-D-wj5mPl.esm.js.map} +1 -1
  38. package/dist/{getSignedSessionId-Cwmt4BkY.cjs → getSignedSessionId-CCHKjoeP.cjs} +3 -3
  39. package/dist/{getSignedSessionId-Cwmt4BkY.cjs.map → getSignedSessionId-CCHKjoeP.cjs.map} +1 -1
  40. package/dist/{getSignedSessionId-S6CBWkOn.native.esm.js → getSignedSessionId-DdKTM3Rh.native.esm.js} +3 -3
  41. package/dist/{getSignedSessionId-S6CBWkOn.native.esm.js.map → getSignedSessionId-DdKTM3Rh.native.esm.js.map} +1 -1
  42. package/dist/{getSignedSessionId-D2w_IRdt.esm.js → getSignedSessionId-Dy04984p.esm.js} +3 -3
  43. package/dist/{getSignedSessionId-D2w_IRdt.esm.js.map → getSignedSessionId-Dy04984p.esm.js.map} +1 -1
  44. package/dist/{getVerifiedCredentialForWalletAccount-QwfkKHfr.esm.js → getVerifiedCredentialForWalletAccount-BFCM2wnx.esm.js} +3 -10
  45. package/dist/getVerifiedCredentialForWalletAccount-BFCM2wnx.esm.js.map +1 -0
  46. package/dist/{getVerifiedCredentialForWalletAccount-CDuP1kaI.cjs → getVerifiedCredentialForWalletAccount-BtIVDer3.cjs} +4 -17
  47. package/dist/getVerifiedCredentialForWalletAccount-BtIVDer3.cjs.map +1 -0
  48. package/dist/{getVerifiedCredentialForWalletAccount-D33r7Drd.native.esm.js → getVerifiedCredentialForWalletAccount-Dsca691m.native.esm.js} +3 -236
  49. package/dist/getVerifiedCredentialForWalletAccount-Dsca691m.native.esm.js.map +1 -0
  50. package/dist/getWaasWalletProviderFromWalletAccount-5-EJK6a_.cjs +19 -0
  51. package/dist/getWaasWalletProviderFromWalletAccount-5-EJK6a_.cjs.map +1 -0
  52. package/dist/getWaasWalletProviderFromWalletAccount-AhhyxkKQ.esm.js +14 -0
  53. package/dist/getWaasWalletProviderFromWalletAccount-AhhyxkKQ.esm.js.map +1 -0
  54. package/dist/getWaasWalletProviderFromWalletAccount-EnCRdI3w.native.esm.js +14 -0
  55. package/dist/getWaasWalletProviderFromWalletAccount-EnCRdI3w.native.esm.js.map +1 -0
  56. package/dist/index.cjs +65 -15
  57. package/dist/index.cjs.map +1 -1
  58. package/dist/index.esm.js +61 -11
  59. package/dist/index.esm.js.map +1 -1
  60. package/dist/index.native.esm.js +64 -14
  61. package/dist/index.native.esm.js.map +1 -1
  62. package/dist/{isMfaRequiredForAction-5p3i98-Z.esm.js → isMfaRequiredForAction-BrmFODJQ.esm.js} +2 -2
  63. package/dist/{isMfaRequiredForAction-5p3i98-Z.esm.js.map → isMfaRequiredForAction-BrmFODJQ.esm.js.map} +1 -1
  64. package/dist/{isMfaRequiredForAction-cmElIVof.native.esm.js → isMfaRequiredForAction-DAfjBLvz.native.esm.js} +2 -2
  65. package/dist/{isMfaRequiredForAction-cmElIVof.native.esm.js.map → isMfaRequiredForAction-DAfjBLvz.native.esm.js.map} +1 -1
  66. package/dist/{isMfaRequiredForAction-z74isCQ7.cjs → isMfaRequiredForAction-aX0vdwv-.cjs} +2 -2
  67. package/dist/{isMfaRequiredForAction-z74isCQ7.cjs.map → isMfaRequiredForAction-aX0vdwv-.cjs.map} +1 -1
  68. package/dist/modules/balances/getBalances/getBalances.d.ts +5 -2
  69. package/dist/modules/balances/getBalances/getBalances.d.ts.map +1 -1
  70. package/dist/modules/balances/getMultichainBalances/getMultichainBalances.d.ts +6 -3
  71. package/dist/modules/balances/getMultichainBalances/getMultichainBalances.d.ts.map +1 -1
  72. package/dist/modules/checkout/checkout.types.d.ts +6 -6
  73. package/dist/modules/checkout/checkout.types.d.ts.map +1 -1
  74. package/dist/modules/initializeClient/initializeClient.d.ts.map +1 -1
  75. package/dist/modules/initializeClient/state.d.ts +9 -0
  76. package/dist/modules/initializeClient/state.d.ts.map +1 -1
  77. package/dist/modules/screenshotProtection/setScreenshotProtection/setScreenshotProtection.d.ts +15 -0
  78. package/dist/modules/screenshotProtection/setScreenshotProtection/setScreenshotProtection.d.ts.map +1 -0
  79. package/dist/modules/screenshotProtection/setScreenshotProtection/setScreenshotProtection.types.d.ts +14 -0
  80. package/dist/modules/screenshotProtection/setScreenshotProtection/setScreenshotProtection.types.d.ts.map +1 -0
  81. package/dist/modules/state/raiseStateEvents/events.d.ts +4 -0
  82. package/dist/modules/state/raiseStateEvents/events.d.ts.map +1 -1
  83. package/dist/modules/waas/createWaasClient/createNativeWaasSDKContainer.d.ts +15 -0
  84. package/dist/modules/waas/createWaasClient/createNativeWaasSDKContainer.d.ts.map +1 -0
  85. package/dist/modules/waas/createWaasProvider/createWaasProvider.d.ts.map +1 -1
  86. package/dist/modules/waas/exportWaasPrivateKeyToContainer/exportWaasPrivateKeyToContainer.d.ts +33 -0
  87. package/dist/modules/waas/exportWaasPrivateKeyToContainer/exportWaasPrivateKeyToContainer.d.ts.map +1 -0
  88. package/dist/modules/waas/exportWaasPrivateKeyToContainer/index.d.ts +2 -0
  89. package/dist/modules/waas/exportWaasPrivateKeyToContainer/index.d.ts.map +1 -0
  90. package/dist/modules/waas/waas.types.d.ts +3 -1
  91. package/dist/modules/waas/waas.types.d.ts.map +1 -1
  92. package/dist/modules/wallets/networks/getBalance/getBalance.d.ts +5 -1
  93. package/dist/modules/wallets/networks/getBalance/getBalance.d.ts.map +1 -1
  94. package/dist/{NotWaasWalletAccountError-CHr72wTH.esm.js → refreshAuth-B2kFf0pl.native.esm.js} +4 -18
  95. package/dist/refreshAuth-B2kFf0pl.native.esm.js.map +1 -0
  96. package/dist/{NotWaasWalletAccountError-CRmyVoB3.native.esm.js → refreshAuth-B8xd2JUm.esm.js} +4 -18
  97. package/dist/refreshAuth-B8xd2JUm.esm.js.map +1 -0
  98. package/dist/{NotWaasWalletAccountError-i1FozGep.cjs → refreshAuth-BMtIp1do.cjs} +3 -23
  99. package/dist/refreshAuth-BMtIp1do.cjs.map +1 -0
  100. package/dist/services/instrumentation/connectLoggerToInstrumentation/connectLoggerToInstrumentation.d.ts.map +1 -1
  101. package/dist/services/instrumentation/instrumentFunction/instrumentFunction.d.ts.map +1 -1
  102. package/dist/services/instrumentation/instrumentation.types.d.ts +6 -0
  103. package/dist/services/instrumentation/instrumentation.types.d.ts.map +1 -1
  104. package/dist/tsconfig.lib.tsbuildinfo +1 -1
  105. package/dist/utils/getNonce/fetchAndStoreNonces/fetchAndStoreNonces.d.ts +4 -0
  106. package/dist/utils/getNonce/fetchAndStoreNonces/fetchAndStoreNonces.d.ts.map +1 -1
  107. package/dist/utils/getNonce/state.d.ts +5 -0
  108. package/dist/utils/getNonce/state.d.ts.map +1 -1
  109. package/dist/utils/getOperatingSystem/getOperatingSystem.d.ts +13 -0
  110. package/dist/utils/getOperatingSystem/getOperatingSystem.d.ts.map +1 -0
  111. package/dist/utils/getOperatingSystem/getOperatingSystem.types.d.ts +8 -0
  112. package/dist/utils/getOperatingSystem/getOperatingSystem.types.d.ts.map +1 -0
  113. package/dist/utils/getPlatform/getPlatform.d.ts +11 -0
  114. package/dist/utils/getPlatform/getPlatform.d.ts.map +1 -0
  115. package/dist/utils/getPlatform/getPlatform.types.d.ts +8 -0
  116. package/dist/utils/getPlatform/getPlatform.types.d.ts.map +1 -0
  117. package/dist/waas.cjs +20 -28
  118. package/dist/waas.cjs.map +1 -1
  119. package/dist/waas.esm.js +4 -12
  120. package/dist/waas.esm.js.map +1 -1
  121. package/dist/waas.native.esm.js +4 -12
  122. package/dist/waas.native.esm.js.map +1 -1
  123. package/dist/waasCore.cjs +39 -4
  124. package/dist/waasCore.cjs.map +1 -1
  125. package/dist/waasCore.esm.js +39 -5
  126. package/dist/waasCore.esm.js.map +1 -1
  127. package/dist/waasCore.native.esm.js +68 -10
  128. package/dist/waasCore.native.esm.js.map +1 -1
  129. package/ios/DynamicClientScreenshotProtection.h +4 -0
  130. package/ios/DynamicClientScreenshotProtection.mm +137 -0
  131. package/package.json +14 -7
  132. package/src/turboModules/NativeScreenshotProtection.native.spec.ts +57 -0
  133. package/src/turboModules/NativeScreenshotProtection.ts +26 -0
  134. package/android/gradle/wrapper/gradle-wrapper.jar +0 -0
  135. package/android/gradle/wrapper/gradle-wrapper.properties +0 -7
  136. package/android/gradlew +0 -251
  137. package/android/gradlew.bat +0 -94
  138. package/android/settings.gradle +0 -30
  139. package/android/src/test/java/xyz/dynamic/client/keychain/KeyStoreKeyManagerTest.kt +0 -288
  140. package/android/src/test/java/xyz/dynamic/client/webview/DynamicWebViewClientTest.kt +0 -196
  141. package/android/src/test/java/xyz/dynamic/client/webview/DynamicWebViewHostOpenTest.kt +0 -347
  142. package/android/src/test/java/xyz/dynamic/client/webview/DynamicWebViewHostTest.kt +0 -171
  143. package/android/src/test/java/xyz/dynamic/client/webview/DynamicWebViewModuleTest.kt +0 -191
  144. package/dist/InvalidParamError-BKnC9rcJ.esm.js.map +0 -1
  145. package/dist/InvalidParamError-Btv8VGSa.native.esm.js.map +0 -1
  146. package/dist/InvalidParamError-DGBih-LJ.cjs.map +0 -1
  147. package/dist/NotWaasWalletAccountError-CHr72wTH.esm.js.map +0 -1
  148. package/dist/NotWaasWalletAccountError-CRmyVoB3.native.esm.js.map +0 -1
  149. package/dist/NotWaasWalletAccountError-i1FozGep.cjs.map +0 -1
  150. package/dist/getNetworkProviderFromNetworkId-Bm41knUq.native.esm.js.map +0 -1
  151. package/dist/getVerifiedCredentialForWalletAccount-CDuP1kaI.cjs.map +0 -1
  152. package/dist/getVerifiedCredentialForWalletAccount-D33r7Drd.native.esm.js.map +0 -1
  153. package/dist/getVerifiedCredentialForWalletAccount-QwfkKHfr.esm.js.map +0 -1
  154. package/react-native.config.cjs +0 -14
@@ -1,196 +0,0 @@
1
- package xyz.dynamic.client.webview
2
-
3
- import android.net.Uri
4
- import android.webkit.WebResourceError
5
- import android.webkit.WebResourceRequest
6
- import android.webkit.WebResourceResponse
7
- import android.webkit.WebView
8
- import androidx.webkit.WebViewFeature
9
- import io.mockk.every
10
- import io.mockk.mockk
11
- import io.mockk.mockkStatic
12
- import io.mockk.unmockkAll
13
- import io.mockk.verify
14
- import org.junit.After
15
- import org.junit.Assert.assertFalse
16
- import org.junit.Assert.assertTrue
17
- import org.junit.Before
18
- import org.junit.Test
19
- import org.junit.runner.RunWith
20
- import org.robolectric.RobolectricTestRunner
21
- import org.robolectric.annotation.Config
22
-
23
- // Exercises the WebViewClient → host delegation in isolation: the host is a
24
- // MockK mock so no Activity/WebView runtime is needed. Runs under Robolectric
25
- // because DynamicWebViewClient extends the framework WebViewClient (whose
26
- // android.jar stub constructor throws off-device) and the tests parse Uris.
27
- @RunWith(RobolectricTestRunner::class)
28
- @Config(sdk = [35])
29
- class DynamicWebViewClientTest {
30
-
31
- private lateinit var host: DynamicWebViewHost
32
- private lateinit var client: DynamicWebViewClient
33
-
34
- @Before
35
- fun setUp() {
36
- host = mockk(relaxed = true)
37
- client = DynamicWebViewClient(host)
38
- // WebViewFeature is a static facade over the device WebView; stub it so
39
- // the tests are deterministic. Default: document-start scripts
40
- // unsupported (so onPageStarted falls back to injecting the bridge).
41
- mockkStatic(WebViewFeature::class)
42
- every { WebViewFeature.isFeatureSupported(any()) } returns false
43
- }
44
-
45
- @After
46
- fun tearDown() {
47
- unmockkAll()
48
- }
49
-
50
- // region navigation pinning
51
-
52
- private fun mainFrameRequestTo(target: String): WebResourceRequest =
53
- mockk {
54
- every { isForMainFrame } returns true
55
- every { url } returns Uri.parse(target)
56
- }
57
-
58
- @Test
59
- fun shouldOverrideUrlLoading_blocksCrossOriginMainFrameNavigation() {
60
- every { host.expectedOrigin } returns "https://app.dynamicauth.com"
61
-
62
- val handled = client.shouldOverrideUrlLoading(
63
- null,
64
- mainFrameRequestTo("https://evil.example.com/phish"),
65
- )
66
-
67
- // true == "we handled it" == the WebView does NOT perform the navigation.
68
- assertTrue(handled)
69
- }
70
-
71
- @Test
72
- fun shouldOverrideUrlLoading_allowsSameOriginMainFrameNavigation() {
73
- every { host.expectedOrigin } returns "https://app.dynamicauth.com"
74
-
75
- val handled = client.shouldOverrideUrlLoading(
76
- null,
77
- mainFrameRequestTo("https://app.dynamicauth.com/waas-v1/abc"),
78
- )
79
-
80
- assertFalse(handled)
81
- }
82
-
83
- @Test
84
- fun shouldOverrideUrlLoading_ignoresSubframeNavigation() {
85
- every { host.expectedOrigin } returns "https://app.dynamicauth.com"
86
- val request = mockk<WebResourceRequest> {
87
- every { isForMainFrame } returns false
88
- every { url } returns Uri.parse("https://evil.example.com/x")
89
- }
90
-
91
- // Subframe loads are left to the WebView regardless of origin.
92
- assertFalse(client.shouldOverrideUrlLoading(null, request))
93
- }
94
-
95
- @Test
96
- fun shouldOverrideUrlLoading_allowsWhenOriginNotYetPinned() {
97
- every { host.expectedOrigin } returns null
98
-
99
- assertFalse(
100
- client.shouldOverrideUrlLoading(
101
- null,
102
- mainFrameRequestTo("https://anything.example.com"),
103
- ),
104
- )
105
- }
106
-
107
- // endregion
108
-
109
- // region bridge-glue injection
110
-
111
- @Test
112
- fun onPageStarted_injectsBridgeScriptWhenDocumentStartUnsupported() {
113
- every {
114
- WebViewFeature.isFeatureSupported(WebViewFeature.DOCUMENT_START_SCRIPT)
115
- } returns false
116
- val view = mockk<WebView>(relaxed = true)
117
-
118
- client.onPageStarted(view, "https://app.dynamicauth.com", null)
119
-
120
- verify { view.evaluateJavascript(BRIDGE_USER_SCRIPT, null) }
121
- }
122
-
123
- @Test
124
- fun onPageStarted_skipsInjectionWhenDocumentStartSupported() {
125
- every {
126
- WebViewFeature.isFeatureSupported(WebViewFeature.DOCUMENT_START_SCRIPT)
127
- } returns true
128
- val view = mockk<WebView>(relaxed = true)
129
-
130
- client.onPageStarted(view, "https://app.dynamicauth.com", null)
131
-
132
- verify(exactly = 0) { view.evaluateJavascript(any(), any()) }
133
- }
134
-
135
- // endregion
136
-
137
- // region error / lifecycle routing
138
-
139
- @Test
140
- fun onPageCommitVisible_notifiesHost() {
141
- client.onPageCommitVisible(null, "https://app.dynamicauth.com")
142
- verify { host.onCommitVisible() }
143
- }
144
-
145
- @Test
146
- fun onReceivedError_mainFrame_reportsNavigationFailed() {
147
- val request = mockk<WebResourceRequest> { every { isForMainFrame } returns true }
148
- val error = mockk<WebResourceError> { every { description } returns "net::ERR_FAILED" }
149
-
150
- client.onReceivedError(null, request, error)
151
-
152
- verify { host.onNavigationFailed("net::ERR_FAILED") }
153
- }
154
-
155
- @Test
156
- fun onReceivedError_subFrame_isIgnored() {
157
- val request = mockk<WebResourceRequest> { every { isForMainFrame } returns false }
158
- val error = mockk<WebResourceError>(relaxed = true)
159
-
160
- client.onReceivedError(null, request, error)
161
-
162
- verify(exactly = 0) { host.onNavigationFailed(any()) }
163
- }
164
-
165
- @Test
166
- fun onReceivedHttpError_mainFrame4xx_reportsHttpError() {
167
- val request = mockk<WebResourceRequest> { every { isForMainFrame } returns true }
168
- val response = mockk<WebResourceResponse> { every { statusCode } returns 404 }
169
-
170
- client.onReceivedHttpError(null, request, response)
171
-
172
- verify { host.onHttpError(404) }
173
- }
174
-
175
- @Test
176
- fun onReceivedHttpError_below400_isIgnored() {
177
- val request = mockk<WebResourceRequest> { every { isForMainFrame } returns true }
178
- val response = mockk<WebResourceResponse> { every { statusCode } returns 302 }
179
-
180
- client.onReceivedHttpError(null, request, response)
181
-
182
- verify(exactly = 0) { host.onHttpError(any()) }
183
- }
184
-
185
- @Test
186
- fun onRenderProcessGone_notifiesHostAndHandlesCrash() {
187
- val handled = client.onRenderProcessGone(null, null)
188
-
189
- // Returning true tells the framework the crash is handled, preventing a
190
- // host-process crash.
191
- assertTrue(handled)
192
- verify { host.onRenderProcessGone() }
193
- }
194
-
195
- // endregion
196
- }
@@ -1,347 +0,0 @@
1
- package xyz.dynamic.client.webview
2
-
3
- import android.app.Activity
4
- import android.net.Uri
5
- import android.webkit.WebSettings
6
- import android.webkit.WebView
7
- import androidx.webkit.JavaScriptReplyProxy
8
- import androidx.webkit.ProfileStore
9
- import androidx.webkit.WebMessageCompat
10
- import androidx.webkit.WebViewCompat
11
- import androidx.webkit.WebViewFeature
12
- import com.facebook.react.bridge.Arguments
13
- import com.facebook.react.bridge.JavaOnlyMap
14
- import com.facebook.react.bridge.Promise
15
- import com.facebook.react.bridge.ReactApplicationContext
16
- import com.facebook.react.bridge.WritableMap
17
- import io.mockk.CapturingSlot
18
- import io.mockk.Runs
19
- import io.mockk.every
20
- import io.mockk.just
21
- import io.mockk.mockk
22
- import io.mockk.mockkStatic
23
- import io.mockk.slot
24
- import io.mockk.unmockkAll
25
- import io.mockk.verify
26
- import org.junit.After
27
- import org.junit.Assert.assertEquals
28
- import org.junit.Assert.assertFalse
29
- import org.junit.Assert.assertTrue
30
- import org.junit.Before
31
- import org.junit.Test
32
- import org.junit.runner.RunWith
33
- import org.robolectric.Robolectric
34
- import org.robolectric.RobolectricTestRunner
35
- import org.robolectric.Shadows.shadowOf
36
- import org.robolectric.annotation.Config
37
-
38
- // Exercises the open() happy path that constructs a real (Robolectric-shadowed)
39
- // WebView. The androidx.webkit statics (WebViewCompat / WebViewFeature /
40
- // ProfileStore) are mocked because they delegate to the device's System WebView
41
- // provider, which does not exist off-device — so the tests assert that the host
42
- // CALLS them correctly (origin-scoped bridge, document-start glue, per-instance
43
- // profile) and applies the secure WebSettings, rather than exercising the
44
- // provider itself. WEB_MESSAGE_LISTENER is reported supported so open() proceeds.
45
- @RunWith(RobolectricTestRunner::class)
46
- @Config(sdk = [35])
47
- class DynamicWebViewHostOpenTest {
48
-
49
- private lateinit var activity: Activity
50
- private lateinit var reactContext: ReactApplicationContext
51
-
52
- private lateinit var webSlot: CapturingSlot<WebView>
53
- private lateinit var originRulesSlot: CapturingSlot<Set<String>>
54
- private lateinit var listenerSlot: CapturingSlot<WebViewCompat.WebMessageListener>
55
-
56
- @Before
57
- fun setUp() {
58
- activity = Robolectric.buildActivity(Activity::class.java).setup().get()
59
- reactContext = mockk(relaxed = true)
60
- every { reactContext.currentActivity } returns activity
61
- // Run UI-queue work inline so onBridgeMessage delivery is synchronous.
62
- // (The http-error-race tests re-stub this to capture instead.)
63
- every { reactContext.runOnUiQueueThread(any()) } answers { firstArg<Runnable>().run() }
64
-
65
- mockkStatic(Arguments::class)
66
- every { Arguments.createMap() } answers { JavaOnlyMap() }
67
-
68
- mockkStatic(WebViewFeature::class)
69
- // Default: only the (required) bridge feature is available. Document-start
70
- // scripts and multi-profile default off; individual tests opt in.
71
- every { WebViewFeature.isFeatureSupported(any()) } returns false
72
- every {
73
- WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)
74
- } returns true
75
-
76
- mockkStatic(WebViewCompat::class)
77
- webSlot = slot()
78
- originRulesSlot = slot()
79
- listenerSlot = slot()
80
- every {
81
- WebViewCompat.addWebMessageListener(
82
- capture(webSlot),
83
- any(),
84
- capture(originRulesSlot),
85
- capture(listenerSlot),
86
- )
87
- } just Runs
88
- every {
89
- WebViewCompat.addDocumentStartJavaScript(any(), any(), any())
90
- } returns mockk(relaxed = true)
91
- every { WebViewCompat.setProfile(any(), any()) } just Runs
92
- }
93
-
94
- @After
95
- fun tearDown() {
96
- unmockkAll()
97
- }
98
-
99
- private fun host(
100
- instanceId: String = "instance-1",
101
- onMessage: (Any?) -> Unit = {},
102
- onRelease: () -> Unit = {},
103
- ) = DynamicWebViewHost(reactContext, instanceId, onMessage, onRelease)
104
-
105
- private fun openHost(
106
- url: String = "https://app.dynamicauth.com/waas-v1/env",
107
- instanceId: String = "instance-1",
108
- onMessage: (Any?) -> Unit = {},
109
- ): Pair<DynamicWebViewHost, Promise> {
110
- val subject = host(instanceId = instanceId, onMessage = onMessage)
111
- val promise = mockk<Promise>(relaxed = true)
112
- subject.open(url, promise)
113
- return subject to promise
114
- }
115
-
116
- // region secure WebSettings
117
-
118
- @Test
119
- fun open_appliesSecureWebSettings() {
120
- openHost()
121
- val s = webSlot.captured.settings
122
-
123
- assertTrue(s.javaScriptEnabled)
124
- assertTrue(s.domStorageEnabled)
125
- assertFalse(s.allowFileAccess)
126
- assertFalse(s.allowContentAccess)
127
- @Suppress("DEPRECATION")
128
- assertFalse(s.allowFileAccessFromFileURLs)
129
- @Suppress("DEPRECATION")
130
- assertFalse(s.allowUniversalAccessFromFileURLs)
131
- assertEquals(WebSettings.MIXED_CONTENT_NEVER_ALLOW, s.mixedContentMode)
132
- assertFalse(s.javaScriptCanOpenWindowsAutomatically)
133
- // Multi-window is enabled so onCreateWindow fires (CVE-2020-6506
134
- // mitigation: contains iframe window requests instead of letting them
135
- // navigate the top frame).
136
- assertTrue(s.supportMultipleWindows())
137
- }
138
-
139
- // onCreateWindow must deny every window request (popups / target=_blank /
140
- // window.open), so the enabled multi-window support never actually opens one.
141
- @Test
142
- fun open_deniesWindowCreation() {
143
- openHost()
144
- val chromeClient = shadowOf(webSlot.captured).webChromeClient
145
- assertFalse(chromeClient.onCreateWindow(webSlot.captured, false, false, null))
146
- }
147
-
148
- // endregion
149
-
150
- // region origin-scoped bridge + navigation target
151
-
152
- @Test
153
- fun open_registersWebMessageListenerScopedToNormalizedOrigin() {
154
- openHost(url = "https://app.dynamicauth.com:443/waas-v1/env")
155
- // Default https port normalized away → matches the page's origin.
156
- assertEquals(setOf("https://app.dynamicauth.com"), originRulesSlot.captured)
157
- }
158
-
159
- @Test
160
- fun open_loadsTheValidatedUrl() {
161
- openHost(url = "https://app.dynamicauth.com/waas-v1/xyz")
162
- assertEquals(
163
- "https://app.dynamicauth.com/waas-v1/xyz",
164
- shadowOf(webSlot.captured).lastLoadedUrl,
165
- )
166
- }
167
-
168
- @Test
169
- fun open_injectsDocumentStartScriptScopedToOriginWhenSupported() {
170
- every {
171
- WebViewFeature.isFeatureSupported(WebViewFeature.DOCUMENT_START_SCRIPT)
172
- } returns true
173
-
174
- openHost(url = "https://app.dynamicauth.com/x")
175
-
176
- verify {
177
- WebViewCompat.addDocumentStartJavaScript(
178
- any(),
179
- BRIDGE_USER_SCRIPT,
180
- setOf("https://app.dynamicauth.com"),
181
- )
182
- }
183
- }
184
-
185
- @Test
186
- fun open_skipsDocumentStartScriptWhenUnsupported() {
187
- openHost()
188
- verify(exactly = 0) {
189
- WebViewCompat.addDocumentStartJavaScript(any(), any(), any())
190
- }
191
- }
192
-
193
- // endregion
194
-
195
- // region per-instance profile (storage isolation)
196
-
197
- @Test
198
- fun open_assignsPerInstanceProfileWhenMultiProfileSupported() {
199
- every {
200
- WebViewFeature.isFeatureSupported(WebViewFeature.MULTI_PROFILE)
201
- } returns true
202
- val store = mockk<ProfileStore>(relaxed = true)
203
- mockkStatic(ProfileStore::class)
204
- every { ProfileStore.getInstance() } returns store
205
-
206
- openHost(instanceId = "abc")
207
-
208
- verify { store.getOrCreateProfile("dynamic-waas-abc") }
209
- verify { WebViewCompat.setProfile(any(), "dynamic-waas-abc") }
210
- }
211
-
212
- @Test
213
- fun open_skipsProfileWhenMultiProfileUnsupported() {
214
- openHost()
215
- verify(exactly = 0) { WebViewCompat.setProfile(any(), any()) }
216
- }
217
-
218
- // endregion
219
-
220
- // region http-error vs commit race (#8)
221
-
222
- @Test
223
- fun httpErrorClaimsPromiseEvenWhenCommitFiresFirst() {
224
- val deferred = slot<Runnable>()
225
- // Capture the deferred resolve instead of running it inline.
226
- every { reactContext.runOnUiQueueThread(capture(deferred)) } just Runs
227
- val (subject, promise) = openHost()
228
-
229
- subject.onCommitVisible() // schedules the deferred resolve
230
- subject.onHttpError(503) // synchronously claims the pending promise
231
- deferred.captured.run() // deferred resolve runs last — but promise is gone
232
-
233
- verify { promise.reject("webview_http_error", "HTTP 503", any<WritableMap>()) }
234
- verify(exactly = 0) { promise.resolve(any()) }
235
- }
236
-
237
- @Test
238
- fun httpErrorBeforeCommit_reportsHttpError() {
239
- val deferred = slot<Runnable>()
240
- every { reactContext.runOnUiQueueThread(capture(deferred)) } just Runs
241
- val (subject, promise) = openHost()
242
-
243
- subject.onHttpError(404)
244
- subject.onCommitVisible()
245
- if (deferred.isCaptured) deferred.captured.run()
246
-
247
- verify { promise.reject("webview_http_error", "HTTP 404", any<WritableMap>()) }
248
- verify(exactly = 0) { promise.resolve(any()) }
249
- }
250
-
251
- @Test
252
- fun commitVisibleResolvesWhenNoHttpError() {
253
- val (subject, promise) = openHost()
254
- subject.onCommitVisible()
255
- verify { promise.resolve(null) }
256
- }
257
-
258
- // endregion
259
-
260
- // region sendMessage via reply proxy (#9)
261
-
262
- @Test
263
- fun sendMessage_postsJsonViaReplyProxyVerbatimIncludingLineSeparators() {
264
- val (subject, _) = openHost()
265
-
266
- // The page opens the reply channel by posting first; invoke the captured
267
- // listener to deliver a replyProxy to the host.
268
- val proxy = mockk<JavaScriptReplyProxy>(relaxed = true)
269
- val handshake = mockk<WebMessageCompat> { every { data } returns "{\"type\":\"ready\"}" }
270
- listenerSlot.captured.onPostMessage(
271
- webSlot.captured,
272
- handshake,
273
- Uri.parse("https://app.dynamicauth.com"),
274
- true,
275
- proxy,
276
- )
277
-
278
- // A dApp-controlled string containing U+2028 must round-trip verbatim —
279
- // the old evaluateJavascript('...') path missed U+2028/U+2029 and would
280
- // have broken out of the JS string literal.
281
- val payload = JavaOnlyMap().apply {
282
- putString("type", "signMessage")
283
- putString("message", "approve\u2028tx")
284
- }
285
- subject.sendMessage(payload)
286
-
287
- val sent = slot<String>()
288
- verify { proxy.postMessage(capture(sent)) }
289
- assertTrue(sent.captured.contains("\u2028"))
290
- assertTrue(sent.captured.contains("signMessage"))
291
- }
292
-
293
- @Test
294
- fun sendMessage_buffersBeforeReplyChannelThenFlushesInOrder() {
295
- val (subject, _) = openHost()
296
-
297
- // Host sends before the page has posted (no replyProxy yet): both must
298
- // be buffered, not dropped.
299
- subject.sendMessage(JavaOnlyMap().apply { putString("seq", "first") })
300
- subject.sendMessage(JavaOnlyMap().apply { putString("seq", "second") })
301
-
302
- // Page opens the reply channel → buffered messages flush in order.
303
- val proxy = mockk<JavaScriptReplyProxy>(relaxed = true)
304
- val handshake = mockk<WebMessageCompat> { every { data } returns "{\"type\":\"ready\"}" }
305
- listenerSlot.captured.onPostMessage(
306
- webSlot.captured,
307
- handshake,
308
- Uri.parse("https://app.dynamicauth.com"),
309
- true,
310
- proxy,
311
- )
312
-
313
- // The capturing list accumulates invocations in call order.
314
- val sent = mutableListOf<String>()
315
- verify(exactly = 2) { proxy.postMessage(capture(sent)) }
316
- assertEquals(2, sent.size)
317
- assertTrue(sent[0].contains("first"))
318
- assertTrue(sent[1].contains("second"))
319
- }
320
-
321
- @Test
322
- fun sendMessage_doesNotCrashWhenReplyChannelNeverOpens() {
323
- val (subject, _) = openHost()
324
- // No listener callback ever → message stays buffered, no crash.
325
- subject.sendMessage(JavaOnlyMap().apply { putString("type", "early") })
326
- }
327
-
328
- @Test
329
- fun bridgeListener_dropsNonMainFrameMessages() {
330
- var delivered: Any? = "UNSET"
331
- val (subject, _) = openHost(onMessage = { delivered = it })
332
- val proxy = mockk<JavaScriptReplyProxy>(relaxed = true)
333
- val message = mockk<WebMessageCompat> { every { data } returns "{\"type\":\"x\"}" }
334
-
335
- listenerSlot.captured.onPostMessage(
336
- webSlot.captured,
337
- message,
338
- Uri.parse("https://app.dynamicauth.com"),
339
- false, // not main frame
340
- proxy,
341
- )
342
-
343
- assertEquals("UNSET", delivered)
344
- }
345
-
346
- // endregion
347
- }