fix: improve transfer recognition and local data safety
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import java.io.FileInputStream
|
||||
import java.net.URI
|
||||
import java.util.Base64
|
||||
import java.util.Properties
|
||||
|
||||
@@ -120,15 +121,21 @@ val internalReleaseRequested = gradle.startParameter.taskNames.any { requestedTa
|
||||
taskName.contains("InternalRelease", ignoreCase = true)
|
||||
}
|
||||
if (internalReleaseRequested) {
|
||||
val expectedApiUrl = "https://lt.frp-say.com:38012"
|
||||
if (dartDefines["INTERNAL_BUILD"] != "true") {
|
||||
throw GradleException(
|
||||
"Internal release requires --dart-define=INTERNAL_BUILD=true",
|
||||
)
|
||||
}
|
||||
if (dartDefines["API_BASE_URL"] != expectedApiUrl) {
|
||||
val apiBaseUrl = dartDefines["API_BASE_URL"]?.trim().orEmpty()
|
||||
val validApiBaseUrl = runCatching {
|
||||
val uri = URI(apiBaseUrl)
|
||||
uri.scheme in setOf("http", "https") &&
|
||||
!uri.host.isNullOrBlank() &&
|
||||
uri.userInfo == null
|
||||
}.getOrDefault(false)
|
||||
if (!validApiBaseUrl) {
|
||||
throw GradleException(
|
||||
"Internal release requires --dart-define=API_BASE_URL=$expectedApiUrl",
|
||||
"Internal release requires an explicit HTTP(S) --dart-define=API_BASE_URL=<url>",
|
||||
)
|
||||
}
|
||||
if (dartDefines["APP_VERSION"].isNullOrBlank()) {
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDOzCCAiOgAwIBAgIELZVPvzANBgkqhkiG9w0BAQsFADBQMQswCQYDVQQGEwJD
|
||||
TjEtMCsGA1UEAxMkU2FrdXJhRnJwIEF1dG9tYXRpYyBUTFMgc24uNzY0NzU5OTk5
|
||||
MRIwEAYDVQQFEwk3NjQ3NTk5OTkwHhcNMjYwNzIwMDQ0NDM4WhcNMjcwNzIwMDQ0
|
||||
NDM4WjBQMQswCQYDVQQGEwJDTjEtMCsGA1UEAxMkU2FrdXJhRnJwIEF1dG9tYXRp
|
||||
YyBUTFMgc24uNzY0NzU5OTk5MRIwEAYDVQQFEwk3NjQ3NTk5OTkwggEiMA0GCSqG
|
||||
SIb3DQEBAQUAA4IBDwAwggEKAoIBAQC0poNmZC+UWeDWGRjHot86lnA3J5Ueujml
|
||||
GvapcAWUoQBNs1p+tlVedz1Dwo/Hq1u7tHdp3WBDax5naKLFKIz0kQIbCWxDrDTH
|
||||
YNVQ9O7MHcf8dcDeayvo6q9z7PzhVXH/CJTlWx2634RGYbaU5jBjWX4fPFHyXJe0
|
||||
57zQqSIYrxZFozEd9NewELavkjCydI8atSFQNEDtlHziiXXLKlvUk7Sk6Drc2AxU
|
||||
ZYk0/mz6GFbBIeIKLUeDlvoocHQvzC3kK+pn1Ggq+ky2DViGPkzekh8HYiOo+/Wv
|
||||
VRXP9LHp17AsmvsQQM4y2NzSkX/la4R1pgq5OwHw6pHHCeWwvBWTAgMBAAGjHTAb
|
||||
MBkGA1UdEQQSMBCCDmx0LmZycC1zYXkuY29tMA0GCSqGSIb3DQEBCwUAA4IBAQBg
|
||||
I4grSvEqI2RRXlwbRjKlBnlBWgiw51sEuM7Sjq6P8t2IoaGJ5/F3PeT0XWwyTopg
|
||||
hV5hNPU+wOKtVilyNqepljrPQ5XAm3uWp68aIHBuCxh3XOfjetPBPXoisY67AUHH
|
||||
9gilTg24GjZ7koJGfiS0iHmfLtf1rEDUgCl27pX9e2NzMRr9aVAsRkdp6D3esXZL
|
||||
e2aUTgBWRg45PU+26dd/JN738F85nqwdRc16MeTMDDbqyIVmcUnZZVPeGXAmTotk
|
||||
b9pHfmaT3h3YqPDv/rWa9w+AMdc2mBIckPQ3ZV4H95xHEOR56uFQwkfiPQABlBKO
|
||||
2oBDbJB1yhK++6XkhmLj
|
||||
-----END CERTIFICATE-----
|
||||
@@ -5,11 +5,4 @@
|
||||
<certificates src="system"/>
|
||||
</trust-anchors>
|
||||
</base-config>
|
||||
<domain-config cleartextTrafficPermitted="false">
|
||||
<domain includeSubdomains="false">lt.frp-say.com</domain>
|
||||
<trust-anchors>
|
||||
<certificates src="system"/>
|
||||
<certificates src="@raw/sakura_frp_test_ca"/>
|
||||
</trust-anchors>
|
||||
</domain-config>
|
||||
</network-security-config>
|
||||
</network-security-config>
|
||||
|
||||
@@ -374,7 +374,7 @@ object LocalPaymentOcr {
|
||||
packageName = packageName,
|
||||
channel = "local_ocr",
|
||||
amountCents = selectedCents,
|
||||
type = direction,
|
||||
type = if (kind == "transfer") "transfer" else direction,
|
||||
merchant = merchant,
|
||||
orderId = orderId,
|
||||
occurredAtEpochMs = capturedAt,
|
||||
@@ -395,6 +395,10 @@ object LocalPaymentOcr {
|
||||
orderId,
|
||||
PaymentParser.sha256(allText),
|
||||
),
|
||||
transferDirection = direction.takeIf { kind == "transfer" }?.let {
|
||||
if (it == "income") "in" else "out"
|
||||
},
|
||||
counterparty = merchant.takeIf { kind == "transfer" },
|
||||
)
|
||||
val reason = when {
|
||||
highConfidence && amountSource == "expected" -> "expected_amount_fallback"
|
||||
@@ -480,4 +484,4 @@ object LocalPaymentOcr {
|
||||
private const val OCR_IDLE_CLOSE_SECONDS = 60L
|
||||
private const val MAX_SCREENSHOT_AGE_MS = 3_000L
|
||||
private const val MAX_AMOUNT = 100_000_000.0
|
||||
}
|
||||
}
|
||||
|
||||
+9
-1
@@ -812,7 +812,7 @@ class ScreenshotAccessibilityService : AccessibilityService() {
|
||||
)
|
||||
return
|
||||
}
|
||||
val retryable = outcome.reason in setOf("no_text", "missing_amount")
|
||||
val retryable = isRetryableOcrOutcome(outcome.reason)
|
||||
if (retryable && flow.retryCount < MAX_VISUAL_RETRIES) {
|
||||
flow.retryCount += 1
|
||||
scheduleVisualRecognition(
|
||||
@@ -1205,6 +1205,14 @@ class ScreenshotAccessibilityService : AccessibilityService() {
|
||||
internal fun shouldSuppressCompletedResult(resultSurfaceExited: Boolean): Boolean =
|
||||
!resultSurfaceExited
|
||||
|
||||
internal fun isRetryableOcrOutcome(reason: String): Boolean = reason in setOf(
|
||||
"no_text",
|
||||
"missing_amount",
|
||||
"expected_amount_missing",
|
||||
"no_success_status",
|
||||
"payment_input_page",
|
||||
)
|
||||
|
||||
internal fun completedResultStartReason(
|
||||
hasObservedResult: Boolean,
|
||||
resultFingerprintChanged: Boolean,
|
||||
|
||||
@@ -307,6 +307,15 @@ class PaymentParserTest {
|
||||
assertFalse(ScreenshotAccessibilityService.shouldSuppressCompletedResult(true))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun transientWechatOcrSurfacesAreRetriedInsteadOfFinallyRejected() {
|
||||
assertTrue(ScreenshotAccessibilityService.isRetryableOcrOutcome("no_success_status"))
|
||||
assertTrue(ScreenshotAccessibilityService.isRetryableOcrOutcome("payment_input_page"))
|
||||
assertTrue(ScreenshotAccessibilityService.isRetryableOcrOutcome("missing_amount"))
|
||||
assertFalse(ScreenshotAccessibilityService.isRetryableOcrOutcome("blocked_status"))
|
||||
assertFalse(ScreenshotAccessibilityService.isRetryableOcrOutcome("history_page"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun consecutiveIdenticalTransfersKeepDistinctFlowIdentities() {
|
||||
val first = paymentSignal(
|
||||
|
||||
Reference in New Issue
Block a user