github BlinkReceipt/blinkreceipt-android 2.4.0

4 hours ago

Activation SDK (1.2.0)

  • Bundled Activation SDK updated from 1.1.2 to 1.2.0. This is the only module in this release with breaking changes; every Receipts SDK module below is source- and binary-compatible with 2.3.1.
  • A receipt that can't be accepted no longer ends the offer flow or reaches your app as an error. Seven cases are now handled inside the SDK across two tiers — on device, before anything is submitted: an unreadable photo, a missing purchase date or total, and a receipt outside the rewards window; from the server: a submission limit, a duplicate, an actionable fraud flag, and the age check again. Each raises a screen that explains the problem and offers to send the receipt to a human reviewer, and dismissing it returns the shopper to the offer wall with the flow intact. Because device-tier checks run before submission, a receipt one of them stops is never registered with the backend and can never come back later as a duplicate.
    • BREAKING CHANGE: ActivationException.ReceiptAlreadyScanned, ReceiptTooOld, ReceiptSubmissionLimitReached, and ReceiptInvalid were removed. Each of those verdicts now has its own screen, so none is an error any more and none reaches onException. Delete those branches — there is nothing to replace them with — and remove any dialog you showed for them, since the SDK now raises its own screen with a review option and yours would compete with it. CameraScan, Submit, ReceiptSummary, and the Registration hierarchy are unchanged.
  • A "Retake photo" now adds a frame to the receipt already in hand rather than starting over. New OffersWallFlow.onResumeScan is invoked in place of onScanReceipt when a shopper adds another photo from a review screen; resume your still-open recognizer session and return the updated result for the same scan, preserving its blinkReceiptId. Left null, the flow falls back to onScanReceipt, which starts a fresh scan and changes the id — nothing errors, but one physical receipt then reaches your backend and your webhooks as two unrelated receipts. New OffersWallFlow.onScanAccepted fires once a completed scan is accepted for processing and no further photo will be requested, which is the point at which you can safely finalize a still-open recognizer session.
    • BREAKING CHANGE: onResumeScan and onScanAccepted were inserted into OffersWallFlow directly after onScanReceipt, so code that builds the flow by position no longer lines up — use named arguments. Both are optional and default to null, so an integration already using named arguments compiles unchanged.
  • A scan session now covers exactly one scan. A scanSessionId opens when the camera opens and closes when the camera exits, including a cancelled scan that never produced a receipt. At most one blinkReceiptId ever appears under one session, so two receipts scanned in a single visit to the wall carry two different ids with no session open in between, and while a shopper is only browsing no session exists at all. Sessions were previously scoped to the offer wall's lifetime, so several receipts in one visit shared a single id and reward, review, and submission events from different receipts could not be told apart.
    • BREAKING CHANGE: startScanSession() and endScanSession() must no longer be called around a scan when you use OffersWall or OffersWallFlow. This is behavioral, not a compile break — the methods still exist and remain supported for hosts that drive scanning without an offer wall. A session you open yourself is one the camera rides along on rather than owns, and a session the camera does not own is never registered with the backend.
  • OffersWall.onScanReceipt semantics depend on who owns the camera. With OffersWall and .activation(true), launch your camera flow and return immediately, exactly as before — do not await the launcher result, since awaiting holds a session open across the camera launch, which is the same mistake as bracketing it by hand. If instead your own code both scans and submits the receipt, the wall's session is the only one there is and the callback must suspend for the whole scan, or the receipt is submitted without a session and rejected. OffersWallFlow is unaffected: its onScanReceipt returns a ScanReceiptResult, so it already suspends by construction.
  • A scan that ends at a review request now earns nothing at scan time. The backend holds the base scan reward and grants it only if a reviewer accepts, which may be hours or days later. Design your backend for open-ended reward accrual per blink_receipt_id and credit the shopper on the resulting RewardUpdate webhook rather than treating the receipt summary as final. The amount itself is unchanged — whatever your app configured through ScanReward. A missing-fields review is the exception: it continues on to processing, so that scan's reward still lands immediately.
  • A missing store name alone no longer blocks a submission. When the merchant is the only field the scanner couldn't read, the receipt submits and the merchant is resolved server-side from the photo — the shopper is never asked to type or pick it. When merchant is missing alongside a date or total, it now shows as already available in the checkmarks and stays out of the manual-entry field. Corrections a shopper makes travel to the reviewer only; they are never merged into the scan submitted for processing, which is always exactly what the scanner read, so treat a correction as a pending claim in your own accounting and wait for the webhook.
  • A clearer path to reporting a missing reward. The icon in the top right of the Receipt Summary is now a Report button, and a "Missing rewards? / Update your receipt" row appears above Your Receipt. Both open the existing Missed Earnings screen and now show on every receipt summary rather than only those missing a merchant, and both are hidden on duplicate, invalid, and pending-review summaries, which offer the banner's review option instead. Missed Earnings itself no longer has a read-only mode: it opens in edit mode instead of requiring a toolbar pencil tap, and Submit moved from a checkmark in the app bar to a full-width footer button.
  • Theming for the new screens. Activation.appearance gains a sixth per-screen record, ActivationAppearance.FrequencyCap, with nine color keys for the "You've hit your limit" screen, and ActivationTheme.Icons gains frequencyCapIcon for its header glyph. ActivationAppearance.ReceiptSummary.Colors gains seven postScanDuplicateBanner* keys for the post-scan banner — despite the naming, these style all three banner cases: duplicate, invalid receipt, and pending review — plus purchaseRowTertiaryLabelColor for the missing-rewards row. Every new key defaults to the theme token its element already renders with, so unset keys change nothing. On Missed Earnings, missedEarningsNavigationSaveButtonBackground now themes the footer button, while missedEarningsNavigationEditButtonIcon, missedEarningsNavigationEditButtonBackground, missedEarningsNavigationSaveButtonIcon, and theme.icons.submitMissedEarnings no longer render anywhere — they still exist, so nothing breaks; they simply have no effect.
    • BREAKING CHANGE: the seven banner keys were inserted in the middle of the ActivationAppearance.ReceiptSummary.Colors constructor, so positional construction no longer lines up — use named arguments. ActivationAppearance and ActivationTheme.Icons each gained one key too, but both were appended last, so those two still compile positionally.
  • Declare android:windowSoftInputMode="adjustResize" on the Activity that hosts the flow. The new review screens contain text fields and inset themselves from the window's IME insets; without it the keyboard covers the form's input and its submit button. CameraRecognizerActivity already declares this as of 2.4.0, so the OffersWall with .activation(true) path needs no change — this applies to hosts embedding OffersWallFlow in their own Activity.
  • Known issue: do not hold a scan session open across the camera launch — neither by calling startScanSession() yourself nor by awaiting the camera result inside OffersWall.onScanReceipt when the camera Activity owns the session. A session already open when the camera starts is one the camera rides along on instead of owning, so nothing registers it with the backend. Device-tier review requests are filed before the receipt is submitted for processing, so that review request becomes the first thing to reference an unregistered session and fails with "Scan session not found". Nothing throws and nothing reaches onException — the shopper simply cannot submit the review. Addressed in a later release.
  • Fixed rewards and review submissions being reported under whichever scan session happened to be open when the network round trip returned, rather than the one that produced the receipt; fixed ActivationClient.close() leaving the active session open, so an id could be inherited by the next initialize(); fixed a scan abandoned before its results were processed leaving the offer wall's per-scan state populated for the next scan; and fixed the Missed Earnings unsaved-changes confirmation being bypassed by the Android system back control.
  • To exercise the new flows, turn test mode off. TestOptions.Test resolves both validation tiers to accepted — the two device checks that describe the scan itself, an unreadable photo and a missing field, still apply under test mode, but the receipt-age check and every server verdict do not.

Recognizer SDK (2.4.0)

  • Fixed: text from a previous scan could carry into the next one, producing duplicated or merged content in ScanResults. Parser reset is now a consume-once signal, spent at the OCR call itself and handed back if that call throws or returns no result, rather than inferred from the frame index — which could advance on frames that never reached OCR, such as when the recognizer was not yet initialized, a bitmap was unusable, or a side lookup threw. Scan sessions are also generation-tracked, so a frame still queued from a superseded session can no longer consume or restore the reset belonging to the session that replaced it. This was most commonly hit by re-entering the camera screen while a scan was still in flight.
  • Fixed: a merchant-logo detection failure discarded the frame. Logo detection is a side lookup; an exception there is now logged and the frame continues to OCR instead of being dropped.
  • Fixed: frames were lost outright when a caller passed a HARDWARE-config bitmap through ImageClient. BitmapUtils.copy() now falls back to ARGB_8888 when the source bitmap is HARDWARE-config or reports no config, instead of throwing — previously either Bitmap.copy() rejected the mutable copy or the later pixel read failed.
  • Added the receipt-level return flag to the recognizer pipeline: FrameResults.isReturn() and FrameResults.Builder.isReturn(boolean), Receipt.isReturn() and Receipt.isReturn(boolean), and the JNI-populated public field OcrResult.isReturn. A new consumer ProGuard rule keeps com.microblink.OcrResult's no-arg constructor and fields so R8 cannot break the JNI field-set contract. No integration changes are required.
  • Added BitmapFrame.resetParser() and resetParser(boolean), making the OCR-parser-reset decision explicit instead of inferred. When never set, the previous frameIndex() <= 0 behavior still applies, so existing integrations are unaffected; an explicit false on frame 0 is now honored.
  • Recognizer.initialize() now logs a debug warning when it is called while a previous session is still active and its in-progress receipt is about to be discarded.
  • Updated the bundled parser to v1.55.1.
  • Stability fixes and improvements

Account Linking SDK (2.4.0)

  • Stability fixes and improvements

Camera SDK (2.4.0)

  • Added CameraView.suspendCamera() and resumeCamera(LifecycleOwner), a sticky unbind alongside the existing transient pauseCamera(). pauseCamera() is undone by the next startCamera() or lifecycle resume; suspendCamera() latches capture suspension so startCamera() no-ops until it is cleared, and is idempotent. resumeCamera(lifecycleOwner) clears the latch and rebinds, and the protected clearCaptureSuspension() clears it without binding, for entry points that run before a LifecycleOwner exists. The latch is checked before the permission gate, so a permission revoked while UI covers the camera cannot fire onPermissionDenied() underneath an overlay.
  • Stability fixes and improvements

Camera UI SDK (2.4.0)

  • Added support for the Activation SDK's new error flows. A new ScanState.CaptureSuspended halts capture while keeping the recognizer session open and the camera frozen on the captured photo, so preliminary results can be handled with two exits — resume capture, adding another photo to the same scan, or terminally finalize. New RecognizerActions back this: PreliminaryResults, ResumeCamera, and ReleaseRecognizer, the last of which terminates the native session without unmounting the camera composable that hosts the action and result collectors. Activation scans only.
  • Fixed: returning from the background re-requested camera permission and re-ran scan initialization on a scan already in progress. Permission is now requested once per camera screen rather than on every foreground resume, and scan observer registration was hardened so repeated initialization can no longer stack duplicate collectors on the same flows — scan state and captured-frame updates could otherwise be handled more than once.
  • Fixed: a stale, already-terminal scan session could be adopted by the next camera visit. Scan session lifetime is now explicitly owned — CameraRecognizerFragment tracks whether it started the session across configuration changes via instance state, ends it in onDestroy only when it isn't a config change, and never ends a session adopted from a host that brackets its own, such as OffersWall.onScanReceipt. All teardown routes through a single internal path.
  • Fixed: delivering a scan result could race the terminal finish out of existence. The session now emits results only after the recognizer acknowledges its end-of-session work, and the preliminary-results and terminal-work watchdog budgets are paused while the host is stopped.
  • Fixed the live camera being briefly visible during the Activation SDK's Receipt Summary teardown by painting an opaque backdrop between the camera and the activation overlay.
  • Lookup-only emissions now omit undetermined fields instead of showing them as not captured; the final found / not-found determination comes from the settled results mapping.
  • CameraRecognizerActivity now finishes when launched without scan options, instead of logging and leaving a blank Activity.
  • CameraRecognizerActivity's manifest configChanges was widened to also absorb smallestScreenSize, uiMode, locale, fontScale, and density, and it now declares windowSoftInputMode="adjustResize". This is deliberate, so a live scan session survives dark-mode, locale, font-scale, and density changes. Hosts that relied on Activity recreation for those events will no longer get it.
  • Added CameraFlow.md, an authoritative reference for camera composition and teardown covering the LIVE / UNBOUND / RELEASED / DESTROYED conditions, the ScanState-to-camera-condition table, and why visibility and teardown are deliberately separate concerns.
  • Stability fixes and improvements

Core SDK (2.4.0)

  • Added ScanResults.isReturn() and ScanResults.Builder.isReturn(boolean), exposing the parser's receipt-level return verdict end-to-end across the camera, direct-OCR, and PDF paths, as well as the Linux OCR-fallback response and the server receipt record behind web and missed-earnings receipts. Defaults to false, and mirrors the iOS implementation. No integration changes are required.
    • The ScanResults Parcel layout changed to carry the new flag, which is written and read mid-stream rather than appended at the end. In-process parceling through Intents, Bundles, or SavedStateHandle is unaffected, since the writer and reader are always the same SDK version, and the SDK never persists Parcel blobs itself. Only an integration that marshals ScanResults to raw bytes and persists them across an SDK upgrade would misread the stream; invalidate that cache on upgrade. Field-based persistence such as JSON is unaffected and reads pre-existing results as non-returns.
  • Stability fixes and improvements

Digital SDK (2.4.0)

  • Stability fixes and improvements

Digital Analytics SDK (2.4.0)

  • Stability fixes and improvements

Earnings SDK (2.4.0)

  • Stability fixes and improvements

Logcat SDK (2.4.0)

  • Added LogEvent.error(() -> String), an open function with an empty default body on the public LogEvent class, backed by LoggingEvents at LogPriority.ERROR. Existing subclasses are unaffected.
  • SDK exception logging was raised from DEBUG to ERROR, so exceptions now survive release-build log filtering. If your crash or log pipeline filters logcat by priority, expect more SDK output at ERROR.
  • Stability fixes and improvements

Security SDK (2.4.0)

  • Stability fixes and improvements

Surveys SDK (2.4.0)

  • Stability fixes and improvements

Don't miss a new blinkreceipt-android release

NewReleases is sending notifications on new releases.