@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.
- package/android/src/main/java/xyz/dynamic/client/DynamicClientPackage.kt +10 -0
- package/android/src/main/java/xyz/dynamic/client/screenshot/ScreenshotProtectionModule.kt +48 -0
- package/dist/{InvalidParamError-DGBih-LJ.cjs → InvalidParamError-C7LLi2cZ.cjs} +91 -13
- package/dist/InvalidParamError-C7LLi2cZ.cjs.map +1 -0
- package/dist/{InvalidParamError-BKnC9rcJ.esm.js → InvalidParamError-DIPFT3sS.esm.js} +73 -13
- package/dist/InvalidParamError-DIPFT3sS.esm.js.map +1 -0
- package/dist/{InvalidParamError-Btv8VGSa.native.esm.js → InvalidParamError-YG7yXCq5.native.esm.js} +294 -13
- package/dist/InvalidParamError-YG7yXCq5.native.esm.js.map +1 -0
- package/dist/NotWaasWalletAccountError-BWrDQFe8.esm.js +18 -0
- package/dist/NotWaasWalletAccountError-BWrDQFe8.esm.js.map +1 -0
- package/dist/NotWaasWalletAccountError-C8ex1YA_.cjs +23 -0
- package/dist/NotWaasWalletAccountError-C8ex1YA_.cjs.map +1 -0
- package/dist/NotWaasWalletAccountError-Cd2o0wr0.native.esm.js +18 -0
- package/dist/NotWaasWalletAccountError-Cd2o0wr0.native.esm.js.map +1 -0
- package/dist/client/core/createCore/getInitialState.d.ts +1 -1
- package/dist/client/core/createCore/getInitialState.d.ts.map +1 -1
- package/dist/client/createDynamicClient/createDynamicClient.d.ts.map +1 -1
- package/dist/client/types/DynamicClient.d.ts +9 -0
- package/dist/client/types/DynamicClient.d.ts.map +1 -1
- package/dist/core.cjs +24 -5
- package/dist/core.cjs.map +1 -1
- package/dist/core.esm.js +24 -6
- package/dist/core.esm.js.map +1 -1
- package/dist/core.native.esm.js +34 -9
- package/dist/core.native.esm.js.map +1 -1
- package/dist/exports/core.d.ts +4 -0
- package/dist/exports/core.d.ts.map +1 -1
- package/dist/exports/index.d.ts +0 -7
- package/dist/exports/index.d.ts.map +1 -1
- package/dist/exports/waasCore.d.ts +2 -0
- package/dist/exports/waasCore.d.ts.map +1 -1
- package/dist/{getNetworkProviderFromNetworkId-Bm41knUq.native.esm.js → getNetworkProviderFromNetworkId-C46TeXs8.native.esm.js} +17 -12
- package/dist/getNetworkProviderFromNetworkId-C46TeXs8.native.esm.js.map +1 -0
- package/dist/{getNetworkProviderFromNetworkId-BdgI7g5U.cjs → getNetworkProviderFromNetworkId-CBGWQS_K.cjs} +4 -4
- package/dist/{getNetworkProviderFromNetworkId-BdgI7g5U.cjs.map → getNetworkProviderFromNetworkId-CBGWQS_K.cjs.map} +1 -1
- package/dist/{getNetworkProviderFromNetworkId-CRuh5pgc.esm.js → getNetworkProviderFromNetworkId-D-wj5mPl.esm.js} +3 -3
- package/dist/{getNetworkProviderFromNetworkId-CRuh5pgc.esm.js.map → getNetworkProviderFromNetworkId-D-wj5mPl.esm.js.map} +1 -1
- package/dist/{getSignedSessionId-Cwmt4BkY.cjs → getSignedSessionId-CCHKjoeP.cjs} +3 -3
- package/dist/{getSignedSessionId-Cwmt4BkY.cjs.map → getSignedSessionId-CCHKjoeP.cjs.map} +1 -1
- package/dist/{getSignedSessionId-S6CBWkOn.native.esm.js → getSignedSessionId-DdKTM3Rh.native.esm.js} +3 -3
- package/dist/{getSignedSessionId-S6CBWkOn.native.esm.js.map → getSignedSessionId-DdKTM3Rh.native.esm.js.map} +1 -1
- package/dist/{getSignedSessionId-D2w_IRdt.esm.js → getSignedSessionId-Dy04984p.esm.js} +3 -3
- package/dist/{getSignedSessionId-D2w_IRdt.esm.js.map → getSignedSessionId-Dy04984p.esm.js.map} +1 -1
- package/dist/{getVerifiedCredentialForWalletAccount-QwfkKHfr.esm.js → getVerifiedCredentialForWalletAccount-BFCM2wnx.esm.js} +3 -10
- package/dist/getVerifiedCredentialForWalletAccount-BFCM2wnx.esm.js.map +1 -0
- package/dist/{getVerifiedCredentialForWalletAccount-CDuP1kaI.cjs → getVerifiedCredentialForWalletAccount-BtIVDer3.cjs} +4 -17
- package/dist/getVerifiedCredentialForWalletAccount-BtIVDer3.cjs.map +1 -0
- package/dist/{getVerifiedCredentialForWalletAccount-D33r7Drd.native.esm.js → getVerifiedCredentialForWalletAccount-Dsca691m.native.esm.js} +3 -236
- package/dist/getVerifiedCredentialForWalletAccount-Dsca691m.native.esm.js.map +1 -0
- package/dist/getWaasWalletProviderFromWalletAccount-5-EJK6a_.cjs +19 -0
- package/dist/getWaasWalletProviderFromWalletAccount-5-EJK6a_.cjs.map +1 -0
- package/dist/getWaasWalletProviderFromWalletAccount-AhhyxkKQ.esm.js +14 -0
- package/dist/getWaasWalletProviderFromWalletAccount-AhhyxkKQ.esm.js.map +1 -0
- package/dist/getWaasWalletProviderFromWalletAccount-EnCRdI3w.native.esm.js +14 -0
- package/dist/getWaasWalletProviderFromWalletAccount-EnCRdI3w.native.esm.js.map +1 -0
- package/dist/index.cjs +65 -15
- package/dist/index.cjs.map +1 -1
- package/dist/index.esm.js +61 -11
- package/dist/index.esm.js.map +1 -1
- package/dist/index.native.esm.js +64 -14
- package/dist/index.native.esm.js.map +1 -1
- package/dist/{isMfaRequiredForAction-5p3i98-Z.esm.js → isMfaRequiredForAction-BrmFODJQ.esm.js} +2 -2
- package/dist/{isMfaRequiredForAction-5p3i98-Z.esm.js.map → isMfaRequiredForAction-BrmFODJQ.esm.js.map} +1 -1
- package/dist/{isMfaRequiredForAction-cmElIVof.native.esm.js → isMfaRequiredForAction-DAfjBLvz.native.esm.js} +2 -2
- package/dist/{isMfaRequiredForAction-cmElIVof.native.esm.js.map → isMfaRequiredForAction-DAfjBLvz.native.esm.js.map} +1 -1
- package/dist/{isMfaRequiredForAction-z74isCQ7.cjs → isMfaRequiredForAction-aX0vdwv-.cjs} +2 -2
- package/dist/{isMfaRequiredForAction-z74isCQ7.cjs.map → isMfaRequiredForAction-aX0vdwv-.cjs.map} +1 -1
- package/dist/modules/balances/getBalances/getBalances.d.ts +5 -2
- package/dist/modules/balances/getBalances/getBalances.d.ts.map +1 -1
- package/dist/modules/balances/getMultichainBalances/getMultichainBalances.d.ts +6 -3
- package/dist/modules/balances/getMultichainBalances/getMultichainBalances.d.ts.map +1 -1
- package/dist/modules/checkout/checkout.types.d.ts +6 -6
- package/dist/modules/checkout/checkout.types.d.ts.map +1 -1
- package/dist/modules/initializeClient/initializeClient.d.ts.map +1 -1
- package/dist/modules/initializeClient/state.d.ts +9 -0
- package/dist/modules/initializeClient/state.d.ts.map +1 -1
- package/dist/modules/screenshotProtection/setScreenshotProtection/setScreenshotProtection.d.ts +15 -0
- package/dist/modules/screenshotProtection/setScreenshotProtection/setScreenshotProtection.d.ts.map +1 -0
- package/dist/modules/screenshotProtection/setScreenshotProtection/setScreenshotProtection.types.d.ts +14 -0
- package/dist/modules/screenshotProtection/setScreenshotProtection/setScreenshotProtection.types.d.ts.map +1 -0
- package/dist/modules/state/raiseStateEvents/events.d.ts +4 -0
- package/dist/modules/state/raiseStateEvents/events.d.ts.map +1 -1
- package/dist/modules/waas/createWaasClient/createNativeWaasSDKContainer.d.ts +15 -0
- package/dist/modules/waas/createWaasClient/createNativeWaasSDKContainer.d.ts.map +1 -0
- package/dist/modules/waas/createWaasProvider/createWaasProvider.d.ts.map +1 -1
- package/dist/modules/waas/exportWaasPrivateKeyToContainer/exportWaasPrivateKeyToContainer.d.ts +33 -0
- package/dist/modules/waas/exportWaasPrivateKeyToContainer/exportWaasPrivateKeyToContainer.d.ts.map +1 -0
- package/dist/modules/waas/exportWaasPrivateKeyToContainer/index.d.ts +2 -0
- package/dist/modules/waas/exportWaasPrivateKeyToContainer/index.d.ts.map +1 -0
- package/dist/modules/waas/waas.types.d.ts +3 -1
- package/dist/modules/waas/waas.types.d.ts.map +1 -1
- package/dist/modules/wallets/networks/getBalance/getBalance.d.ts +5 -1
- package/dist/modules/wallets/networks/getBalance/getBalance.d.ts.map +1 -1
- package/dist/{NotWaasWalletAccountError-CHr72wTH.esm.js → refreshAuth-B2kFf0pl.native.esm.js} +4 -18
- package/dist/refreshAuth-B2kFf0pl.native.esm.js.map +1 -0
- package/dist/{NotWaasWalletAccountError-CRmyVoB3.native.esm.js → refreshAuth-B8xd2JUm.esm.js} +4 -18
- package/dist/refreshAuth-B8xd2JUm.esm.js.map +1 -0
- package/dist/{NotWaasWalletAccountError-i1FozGep.cjs → refreshAuth-BMtIp1do.cjs} +3 -23
- package/dist/refreshAuth-BMtIp1do.cjs.map +1 -0
- package/dist/services/instrumentation/connectLoggerToInstrumentation/connectLoggerToInstrumentation.d.ts.map +1 -1
- package/dist/services/instrumentation/instrumentFunction/instrumentFunction.d.ts.map +1 -1
- package/dist/services/instrumentation/instrumentation.types.d.ts +6 -0
- package/dist/services/instrumentation/instrumentation.types.d.ts.map +1 -1
- package/dist/tsconfig.lib.tsbuildinfo +1 -1
- package/dist/utils/getNonce/fetchAndStoreNonces/fetchAndStoreNonces.d.ts +4 -0
- package/dist/utils/getNonce/fetchAndStoreNonces/fetchAndStoreNonces.d.ts.map +1 -1
- package/dist/utils/getNonce/state.d.ts +5 -0
- package/dist/utils/getNonce/state.d.ts.map +1 -1
- package/dist/utils/getOperatingSystem/getOperatingSystem.d.ts +13 -0
- package/dist/utils/getOperatingSystem/getOperatingSystem.d.ts.map +1 -0
- package/dist/utils/getOperatingSystem/getOperatingSystem.types.d.ts +8 -0
- package/dist/utils/getOperatingSystem/getOperatingSystem.types.d.ts.map +1 -0
- package/dist/utils/getPlatform/getPlatform.d.ts +11 -0
- package/dist/utils/getPlatform/getPlatform.d.ts.map +1 -0
- package/dist/utils/getPlatform/getPlatform.types.d.ts +8 -0
- package/dist/utils/getPlatform/getPlatform.types.d.ts.map +1 -0
- package/dist/waas.cjs +20 -28
- package/dist/waas.cjs.map +1 -1
- package/dist/waas.esm.js +4 -12
- package/dist/waas.esm.js.map +1 -1
- package/dist/waas.native.esm.js +4 -12
- package/dist/waas.native.esm.js.map +1 -1
- package/dist/waasCore.cjs +39 -4
- package/dist/waasCore.cjs.map +1 -1
- package/dist/waasCore.esm.js +39 -5
- package/dist/waasCore.esm.js.map +1 -1
- package/dist/waasCore.native.esm.js +68 -10
- package/dist/waasCore.native.esm.js.map +1 -1
- package/ios/DynamicClientScreenshotProtection.h +4 -0
- package/ios/DynamicClientScreenshotProtection.mm +137 -0
- package/package.json +14 -7
- package/src/turboModules/NativeScreenshotProtection.native.spec.ts +57 -0
- package/src/turboModules/NativeScreenshotProtection.ts +26 -0
- package/android/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/android/gradle/wrapper/gradle-wrapper.properties +0 -7
- package/android/gradlew +0 -251
- package/android/gradlew.bat +0 -94
- package/android/settings.gradle +0 -30
- package/android/src/test/java/xyz/dynamic/client/keychain/KeyStoreKeyManagerTest.kt +0 -288
- package/android/src/test/java/xyz/dynamic/client/webview/DynamicWebViewClientTest.kt +0 -196
- package/android/src/test/java/xyz/dynamic/client/webview/DynamicWebViewHostOpenTest.kt +0 -347
- package/android/src/test/java/xyz/dynamic/client/webview/DynamicWebViewHostTest.kt +0 -171
- package/android/src/test/java/xyz/dynamic/client/webview/DynamicWebViewModuleTest.kt +0 -191
- package/dist/InvalidParamError-BKnC9rcJ.esm.js.map +0 -1
- package/dist/InvalidParamError-Btv8VGSa.native.esm.js.map +0 -1
- package/dist/InvalidParamError-DGBih-LJ.cjs.map +0 -1
- package/dist/NotWaasWalletAccountError-CHr72wTH.esm.js.map +0 -1
- package/dist/NotWaasWalletAccountError-CRmyVoB3.native.esm.js.map +0 -1
- package/dist/NotWaasWalletAccountError-i1FozGep.cjs.map +0 -1
- package/dist/getNetworkProviderFromNetworkId-Bm41knUq.native.esm.js.map +0 -1
- package/dist/getVerifiedCredentialForWalletAccount-CDuP1kaI.cjs.map +0 -1
- package/dist/getVerifiedCredentialForWalletAccount-D33r7Drd.native.esm.js.map +0 -1
- package/dist/getVerifiedCredentialForWalletAccount-QwfkKHfr.esm.js.map +0 -1
- 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
|
-
}
|