Skip to content

Commit e2bceda

Browse files
committed
fix(onramp): harden Coinbase GPay auto-click and improve error diagnostics
Guard auto-click with AtomicBoolean to prevent duplicate triggers, add fallback delayed click for missed load_success events, search iframes for GPay button, emit diagnostic metadata on button-not-found errors, and simplify internal error copy. Signed-off-by: Brandon McAnsh <git@bmcreations.dev>
1 parent e95bc3c commit e2bceda

4 files changed

Lines changed: 86 additions & 14 deletions

File tree

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,4 +99,4 @@ The feature plugin automatically includes `:libs:logging`, `:ui:core`, `:ui:comp
9999

100100
- Conventional commits: `feat:`, `fix:`, `chore:`, with optional scope in parens (e.g., `feat(oc):`, `fix(tokens):`)
101101
- Main branch: `main`
102-
- CI runs on all PRs (tests via Fastlane)
102+
- CI runs on all PRs (tests via Fastlane)

apps/flipcash/core/src/main/res/values/strings.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -557,7 +557,7 @@
557557
<string name="error_description_onrampTransactionFailed">The Coinbase team has been notified and is investigating the issue. Your funds will arrive once resolved. We appreciate your patience</string>
558558

559559
<string name="error_title_onrampInternal">Something Went Wrong</string>
560-
<string name="error_description_onrampInternal">The Coinbase team has been notified and is investigating the issue. Your funds will arrive once resolved. We appreciate your patience</string>
560+
<string name="error_description_onrampInternal">Please try again. We appreciate your patience</string>
561561

562562
<string name="error_title_onrampTransactionSendFailed">Something Went Wrong</string>
563563
<string name="error_description_onrampTransactionSendFailed">We are working with the Coinbase team to resolve the issue. Your card will be refunded in the meantime. Please try again later</string>

apps/flipcash/shared/onramp/coinbase/src/main/kotlin/com/flipcash/app/onramp/CoinbaseOnRampWebview.kt

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import com.flipcash.app.onramp.internal.CoinbaseOnRampScripts
2020
import com.flipcash.app.onramp.internal.CoinbaseOnRampWebError
2121
import com.flipcash.app.web.ComposeWebView
2222
import com.getcode.utils.trace
23+
import java.util.concurrent.atomic.AtomicBoolean
2324
import kotlin.time.TimeSource
2425

2526
@SuppressLint("SetJavaScriptEnabled", "WrongConstant")
@@ -55,27 +56,28 @@ private fun WebView.configureForCoinbaseOnRamp(
5556
val startMark = TimeSource.Monotonic.markNow()
5657
trace(tag = "CoinbaseOnRamp", message = "WebView configured")
5758

59+
val autoClickTriggered = AtomicBoolean(false)
5860
var messageListenerInstalled = false
61+
5962
settings.javaScriptEnabled = true
6063
settings.domStorageEnabled = true
6164
settings.javaScriptCanOpenWindowsAutomatically = true
6265
settings.mixedContentMode = WebSettings.MIXED_CONTENT_COMPATIBILITY_MODE
6366
settings.setSupportMultipleWindows(false)
64-
settings.userAgentString = settings.userAgentString
65-
.replace("; wv", "")
66-
.replace("Version/4.0 ", "")
6767

6868
val eventHandler = CoinbaseOnRampEventHandler(
6969
startMark = startMark,
7070
onPaymentSuccess = onPaymentSuccess,
7171
onPaymentFailure = onPaymentFailure,
7272
onCancel = onCancel,
7373
onAutoClickGPay = {
74-
post {
75-
evaluateJavascript(
76-
CoinbaseOnRampScripts.AUTO_CLICK_GPAY_BUTTON,
77-
null
78-
)
74+
if (autoClickTriggered.compareAndSet(false, true)) {
75+
post {
76+
evaluateJavascript(
77+
CoinbaseOnRampScripts.AUTO_CLICK_GPAY_BUTTON,
78+
null
79+
)
80+
}
7981
}
8082
},
8183
)
@@ -96,6 +98,15 @@ private fun WebView.configureForCoinbaseOnRamp(
9698
metadata = { "elapsed_ms" to startMark.elapsedNow().inWholeMilliseconds },
9799
)
98100
view?.evaluateJavascript(CoinbaseOnRampScripts.MESSAGE_BRIDGE, null)
101+
102+
// Fallback: if load_success already fired before the bridge was installed,
103+
// trigger auto-click after a delay to give the bridge a chance first.
104+
view?.postDelayed({
105+
if (autoClickTriggered.compareAndSet(false, true)) {
106+
trace(tag = "CoinbaseOnRamp", message = "Fallback auto-click (load_success not received)")
107+
view.evaluateJavascript(CoinbaseOnRampScripts.AUTO_CLICK_GPAY_BUTTON, null)
108+
}
109+
}, 2000)
99110
}
100111

101112
override fun onReceivedError(
@@ -124,5 +135,8 @@ private fun WebView.configureForCoinbaseOnRamp(
124135
CoinbaseOnRampScripts.PAYMENT_REQUEST_INTERCEPTOR,
125136
setOf("*")
126137
)
138+
// MESSAGE_BRIDGE stays in onPageFinished via evaluateJavascript() because
139+
// addEventListener('message') in a document-start isolated world cannot
140+
// observe postMessage events dispatched in the page world.
127141
}
128142
}

apps/flipcash/shared/onramp/coinbase/src/main/kotlin/com/flipcash/app/onramp/internal/CoinbaseOnRampEventHandler.kt

Lines changed: 62 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,13 @@ internal object CoinbaseOnRampScripts {
2323
val PAYMENT_REQUEST_INTERCEPTOR = """
2424
(function() {
2525
if (!window.PaymentRequest) return;
26+
function bridge(data) {
27+
if (typeof AndroidBridge !== 'undefined') AndroidBridge.onEvent(JSON.stringify(data));
28+
}
2629
2730
var origShow = PaymentRequest.prototype.show;
2831
PaymentRequest.prototype.show = function() {
29-
AndroidBridge.onEvent(JSON.stringify({ eventName: 'timing.payment_modal_shown' }));
32+
bridge({ eventName: 'timing.payment_modal_shown' });
3033
return origShow.apply(this, arguments).catch(function(err) {
3134
window.postMessage(JSON.stringify({
3235
eventName: 'onramp_api.load_error',
@@ -81,8 +84,23 @@ internal object CoinbaseOnRampScripts {
8184

8285
val AUTO_CLICK_GPAY_BUTTON = """
8386
(function() {
84-
function tryClick(attempt) {
87+
function findGPayButton() {
8588
var btn = document.getElementById('gpay-button-online-api-id');
89+
if (btn) return btn;
90+
var iframes = document.querySelectorAll('iframe');
91+
for (var i = 0; i < iframes.length; i++) {
92+
try {
93+
var doc = iframes[i].contentDocument;
94+
if (doc) {
95+
btn = doc.getElementById('gpay-button-online-api-id');
96+
if (btn) return btn;
97+
}
98+
} catch(e) {}
99+
}
100+
return null;
101+
}
102+
function tryClick(attempt) {
103+
var btn = findGPayButton();
86104
if (btn) {
87105
var el = btn;
88106
var handlers = null;
@@ -116,9 +134,34 @@ internal object CoinbaseOnRampScripts {
116134
} else if (attempt < 10) {
117135
setTimeout(function() { tryClick(attempt + 1); }, 500);
118136
} else {
137+
var allBtns = document.querySelectorAll('button, [role="button"]');
138+
var gpayEls = document.querySelectorAll('[id*="gpay"], [class*="gpay"], [id*="google-pay"], [class*="google-pay"]');
139+
var iframes = document.querySelectorAll('iframe');
140+
var iframeBtns = 0;
141+
var iframeGpay = 0;
142+
for (var k = 0; k < iframes.length; k++) {
143+
try {
144+
var doc = iframes[k].contentDocument;
145+
if (doc) {
146+
iframeBtns += doc.querySelectorAll('button, [role="button"]').length;
147+
iframeGpay += doc.querySelectorAll('[id*="gpay"], [class*="gpay"]').length;
148+
}
149+
} catch(e) { iframeBtns = -1; }
150+
}
119151
AndroidBridge.onEvent(JSON.stringify({
120152
eventName: 'onramp_api.load_error',
121-
data: { errorCode: 'ERROR_CODE_GOOGLE_PAY_BUTTON_NOT_FOUND' }
153+
data: {
154+
errorCode: 'ERROR_CODE_GOOGLE_PAY_BUTTON_NOT_FOUND',
155+
buttons: allBtns.length,
156+
gpayElements: gpayEls.length,
157+
iframes: iframes.length,
158+
iframeBtns: iframeBtns,
159+
iframeGpay: iframeGpay,
160+
readyState: document.readyState,
161+
bodyChildren: document.body ? document.body.children.length : -1,
162+
viewport: window.innerWidth + 'x' + window.innerHeight,
163+
paymentRequest: typeof PaymentRequest
164+
}
122165
}));
123166
}
124167
}
@@ -162,7 +205,22 @@ internal class CoinbaseOnRampEventHandler(
162205
tag = "CoinbaseOnRamp",
163206
message = "Error during coinbase buy module",
164207
error = error,
165-
type = TraceType.Error
208+
type = TraceType.Error,
209+
metadata = if (errorCode == "ERROR_CODE_GOOGLE_PAY_BUTTON_NOT_FOUND" && data != null) {
210+
{
211+
"buttons" to data.optInt("buttons", -1)
212+
"gpayElements" to data.optInt("gpayElements", -1)
213+
"iframes" to data.optInt("iframes", -1)
214+
"iframeBtns" to data.optInt("iframeBtns", -1)
215+
"iframeGpay" to data.optInt("iframeGpay", -1)
216+
"readyState" to data.optString("readyState", "")
217+
"bodyChildren" to data.optInt("bodyChildren", -1)
218+
"viewport" to data.optString("viewport", "")
219+
"paymentRequest" to data.optString("paymentRequest", "")
220+
}
221+
} else {
222+
{}
223+
},
166224
)
167225

168226
onPaymentFailure(error)

0 commit comments

Comments
 (0)