diff --git a/README.md b/README.md index b7818515..0d2ff2ae 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![All Contributors](https://img.shields.io/badge/all_contributors-4-orange.svg?style=flat-square)](#contributors-) -The TIKI Capture Receipt Plugin for Capacitor enables the extraction of receipt data from photos, email inboxes, and retailer accounts using [Microblink's](https://microblink.com) OCR technology. +The TIKI Capture Receipt Plugin for Capacitor enables the extraction of receipt data from email inboxes, and retailer accounts using [Microblink's](https://microblink.com) technology. This plugin wraps (and simplifies) Microblink's native [iOS](https://github.com/BlinkReceipt/blinkreceipt-ios) and [Android](https://github.com/BlinkReceipt/blinkreceipt-android) SDKs for use with Capacitor applications. @@ -14,9 +14,9 @@ This plugin wraps (and simplifies) Microblink's native [iOS](https://github.com/ 1. Install the dependency from NPM -`npm install @mytiki/tiki-capture-receipt-capacitor` +`npm install @mytiki/capture-receipt-capacitor` -2. Add Microblink iOS dependencies in `ios/App/Podfile` +2. iOS only - Add Microblink iOS dependencies in `ios/App/Podfile` ``` source 'https://github.com/BlinkReceipt/PodSpecRepo.git' # <- add this @@ -48,6 +48,8 @@ end That's it. And yes, it's really that easy. +_NOTE: if Cocoapods can't find BlinkReceipt/BlinkEReceipt in iOS, run `pod install --repo-udpate` and `npx cap sync` again._ + ## Initialization **IMPORTANT: Requires a valid Microblink BlinkReceipt License Key and Product Intelligence Key. [Reach out to get one →](https://mytiki.com)** @@ -56,23 +58,12 @@ That's it. And yes, it's really that easy. ```ts import { instance } from '@mytiki/tiki-capture-receipt-capacitor' -instance.initialize('', '') +instance.initialize('', '', '', .then((rsp) => console.log(`initialized`)) ``` _NOTE: Only iOS and Android are supported._ -### Google OAuth - -To use Google OAuth for Gmail login provide a valid [Google OAuth Client ID](https://developers.google.com/identity/protocols/oauth2) in initialization: - -```ts -import { instance } from '@mytiki/tiki-capture-receipt-capacitor' - -instance.initialize('', '', '') - .then((rsp) => console.log(`initialized`)) -``` - # Contributing - Use [GitHub Issues](https://github.com/tiki/tiki-capture-receipt-capacitor/issues) to report any bugs you find or to request enhancements. diff --git a/android/src/main/kotlin/com/mytiki/sdk/capture/receipt/capacitor/CaptureReceipt.kt b/android/src/main/kotlin/com/mytiki/sdk/capture/receipt/capacitor/CaptureReceipt.kt index c22a04e9..52267327 100644 --- a/android/src/main/kotlin/com/mytiki/sdk/capture/receipt/capacitor/CaptureReceipt.kt +++ b/android/src/main/kotlin/com/mytiki/sdk/capture/receipt/capacitor/CaptureReceipt.kt @@ -115,11 +115,11 @@ class CaptureReceipt { } else { when (account.accountCommon.type) { AccountTypeEnum.EMAIL -> { - email.remove(context, account, onComplete, onError) + email.logout(context, account, onComplete, onError) } AccountTypeEnum.RETAILER -> { - retailer.remove(context, account, onComplete, onError) + retailer.logout(context, account, onComplete, onError) } } } diff --git a/android/src/main/kotlin/com/mytiki/sdk/capture/receipt/capacitor/email/Email.kt b/android/src/main/kotlin/com/mytiki/sdk/capture/receipt/capacitor/email/Email.kt index 8f91abb4..6fa7e45b 100644 --- a/android/src/main/kotlin/com/mytiki/sdk/capture/receipt/capacitor/email/Email.kt +++ b/android/src/main/kotlin/com/mytiki/sdk/capture/receipt/capacitor/email/Email.kt @@ -6,8 +6,6 @@ package com.mytiki.sdk.capture.receipt.capacitor.email import android.content.Context -import android.os.Build -import androidx.annotation.RequiresApi import androidx.fragment.app.FragmentManager import com.microblink.core.InitializeCallback import com.microblink.core.ScanResults @@ -27,9 +25,7 @@ import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.MainScope import kotlinx.coroutines.async import kotlinx.coroutines.tasks.await -import java.time.Duration import java.util.Calendar -import kotlin.math.abs import kotlin.math.floor typealias OnReceiptCallback = ((receipt: ScanResults?) -> Unit) @@ -137,25 +133,22 @@ class Email { onError: (msg: String) -> Unit, onComplete: () -> Unit ) { - val onRead = { lastScrape: Long -> + MainScope().async { var dayCutOff = 15 val now = Calendar.getInstance().timeInMillis + val lastScrape = context.getImapScanTime().await() val diffInMillis = now - lastScrape - val diffInDays = floor((diffInMillis/86400000).toDouble()).toInt() - if(diffInDays <= 15) { + val diffInDays = floor((diffInMillis / 86400000).toDouble()).toInt() + if (diffInDays <= 15) { dayCutOff = diffInDays } - - this.client(context, onError) { client -> + this@Email.client(context, onError) { client -> client.dayCutoff(dayCutOff) client.messages(object : MessagesCallback { override fun onComplete( credential: PasswordCredentials, result: List ) { - if (result.isEmpty()) { - onError("No results for ${credential.username()} - ${credential.provider()}") - } result.forEach { receipt -> onReceipt(receipt) } @@ -163,6 +156,7 @@ class Email { onComplete() client.close() } + override fun onException(throwable: Throwable) { onError(throwable.message ?: throwable.toString()) onComplete() @@ -171,7 +165,6 @@ class Email { }) } } - context.getImapScanTime(onRead, onError) } /** @@ -197,7 +190,6 @@ class Email { } else { for (credential in credentials) { val account = Account.fromEmailAccount(credential) - account.isVerified = client.verify(credential).await() onAccount(account) returnedAccounts++ @@ -224,7 +216,7 @@ class Email { * @param onRemove Callback called when the account is successfully removed. * @param onError Callback called when an error occurs during account removal. */ - fun remove( + fun logout( context: Context, account: Account, onRemove: () -> Unit, @@ -238,8 +230,9 @@ class Email { ).value } client.logout(passwordCredentials).addOnSuccessListener { - onRemove() + client.clearLastCheckedTime(Provider.valueOf(account.accountCommon.id)) context.deleteImapScanTime() + onRemove() }.addOnFailureListener { onError( it.message @@ -261,15 +254,16 @@ class Email { fun flush(context: Context, onComplete: () -> Unit, onError: (msg: String) -> Unit) { this.client(context, onError) { client -> client.logout().addOnSuccessListener { - onComplete() + client.clearLastCheckedTime() context.deleteImapScanTime() + onComplete() }.addOnFailureListener { onError(it.message ?: it.toString()) } } } - private fun client( + fun client( context: Context, onError: (String) -> Unit, onClientReady: (ImapClient) -> Unit ) { @@ -280,7 +274,6 @@ class Email { override fun onComplete() { clientInitialization.complete(Unit) } - override fun onException(ex: Throwable) { onError(ex.message ?: "Error in IMAP client initialization: $ex") } diff --git a/android/src/main/kotlin/com/mytiki/sdk/capture/receipt/capacitor/email/EmailScanDateHandlers.kt b/android/src/main/kotlin/com/mytiki/sdk/capture/receipt/capacitor/email/EmailScanDateHandlers.kt index d017cd77..4c45dc91 100644 --- a/android/src/main/kotlin/com/mytiki/sdk/capture/receipt/capacitor/email/EmailScanDateHandlers.kt +++ b/android/src/main/kotlin/com/mytiki/sdk/capture/receipt/capacitor/email/EmailScanDateHandlers.kt @@ -2,10 +2,14 @@ package com.mytiki.sdk.capture.receipt.capacitor.email import android.content.Context import androidx.datastore.preferences.core.* +import com.microblink.core.Timberland import com.mytiki.sdk.capture.receipt.capacitor.plugin.dataStore +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.Deferred import kotlinx.coroutines.MainScope import kotlinx.coroutines.async import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map val key = stringPreferencesKey("tiki-captures-receipt.imap-latest-date") @@ -27,17 +31,15 @@ fun Context.setImapScanTime(value: Long) { * @param onComplete Callback function to handle the retrieved [Long] value. * @param onError Callback function to handle errors. */ -fun Context.getImapScanTime(onComplete: (Long) -> Unit, onError: (String) -> Unit) { - MainScope().async { - try { - val date = dataStore.data.map { pref -> - pref[longPreferencesKey(key.name)] ?: 0L - } - date.distinctUntilChanged().collect { value -> - onComplete(value) - } +fun Context.getImapScanTime(): Deferred { + return MainScope().async { + try{ + //dataStore.data.first()[longPreferencesKey(key.name)] ?: 0L + 15 + }catch(ex: Exception){ - onError(ex.message ?: "Error in getting Imap sca time.") + Timberland.d(ex.message ?: "Error in getting Imap scan time.") + 15 } } } @@ -47,7 +49,7 @@ fun Context.getImapScanTime(onComplete: (Long) -> Unit, onError: (String) -> Uni */ fun Context.deleteImapScanTime(){ MainScope().async { - dataStore.edit { pref -> pref.clear() } + dataStore.edit { pref -> pref[longPreferencesKey(key.name)] = 0L } } } diff --git a/android/src/main/kotlin/com/mytiki/sdk/capture/receipt/capacitor/plugin/CaptureReceiptPlugin.kt b/android/src/main/kotlin/com/mytiki/sdk/capture/receipt/capacitor/plugin/CaptureReceiptPlugin.kt index db2d10c2..50aa3444 100644 --- a/android/src/main/kotlin/com/mytiki/sdk/capture/receipt/capacitor/plugin/CaptureReceiptPlugin.kt +++ b/android/src/main/kotlin/com/mytiki/sdk/capture/receipt/capacitor/plugin/CaptureReceiptPlugin.kt @@ -55,10 +55,14 @@ class CaptureReceiptPlugin : Plugin() { */ @PluginMethod fun initialize(call: PluginCall) { - val request = ReqInitialize(call) - captureReceipt.initialize(context, request.licenseKey, request.productKey, { - call.resolve() - }, { error -> call.reject(error) }) + try{ + val request = ReqInitialize(call) + captureReceipt.initialize(context, request.licenseKey, request.productKey, { + call.resolve() + }, { error -> call.reject(error) }) + }catch (error: Error) { + call.reject(error.message); + } } /** @@ -167,12 +171,10 @@ class CaptureReceiptPlugin : Plugin() { * @param scan The scanned results. */ private fun onReceipt(requestId: String, scan: ScanResults? = null) { - val data = if (scan != null) { - RspReceipt(requestId, scan).toJS() - } else { - JSObject() + if (scan != null) { + val data = RspReceipt(requestId, scan).toJS() + notifyListeners("onCapturePluginResult", data) } - notifyListeners("onCapturePluginResult", data) } /** diff --git a/android/src/main/kotlin/com/mytiki/sdk/capture/receipt/capacitor/plugin/req/ReqInitialize.kt b/android/src/main/kotlin/com/mytiki/sdk/capture/receipt/capacitor/plugin/req/ReqInitialize.kt index 3f9d55d4..28882331 100644 --- a/android/src/main/kotlin/com/mytiki/sdk/capture/receipt/capacitor/plugin/req/ReqInitialize.kt +++ b/android/src/main/kotlin/com/mytiki/sdk/capture/receipt/capacitor/plugin/req/ReqInitialize.kt @@ -19,8 +19,8 @@ import com.getcapacitor.PluginCall * @param call A [PluginCall] containing the initialization data. */ class ReqInitialize(call: PluginCall) : Req(call) { - val licenseKey: String val productKey: String + val licenseKey: String /** * Initializes the [ReqInitialize] object by extracting the licenseKey and productKey @@ -29,17 +29,9 @@ class ReqInitialize(call: PluginCall) : Req(call) { * @param data A [JSObject] containing the initialization data. */ init { - val licenseKey = call.getString("licenseKey") - val productKey = call.getString("productKey") - if (licenseKey == null) { - call.reject("Provide a License Key for initialization.") - throw Error("Provide a License Key for initialization.") - } - if (productKey == null) { - call.reject("Provide a Product Intelligence Key for initialization.") - throw Error("Provide a Product Intelligence Key for initialization.") - } - this.productKey = productKey - this.licenseKey = licenseKey + productKey = call.getString("productKey") + ?: throw Error("Provide a Product Intelligence Key for initialization.") + licenseKey = call.getString("android") ?: + throw Error("Provide an Android License Key for initialization.") } } diff --git a/android/src/main/kotlin/com/mytiki/sdk/capture/receipt/capacitor/retailer/Retailer.kt b/android/src/main/kotlin/com/mytiki/sdk/capture/receipt/capacitor/retailer/Retailer.kt index 63c4fcbe..3949822d 100644 --- a/android/src/main/kotlin/com/mytiki/sdk/capture/receipt/capacitor/retailer/Retailer.kt +++ b/android/src/main/kotlin/com/mytiki/sdk/capture/receipt/capacitor/retailer/Retailer.kt @@ -13,6 +13,7 @@ import android.widget.FrameLayout import androidx.appcompat.app.AppCompatActivity import com.microblink.core.InitializeCallback import com.microblink.core.ScanResults +import com.microblink.core.Timberland import com.microblink.linking.AccountLinkingClient import com.microblink.linking.AccountLinkingException import com.microblink.linking.BlinkReceiptLinkingSdk @@ -114,7 +115,7 @@ class Retailer { * @param onError Callback called when an error occurs during account removal. */ @OptIn(ExperimentalCoroutinesApi::class) - fun remove( + fun logout( context: Context, account: Account, onComplete: () -> Unit, @@ -133,7 +134,8 @@ class Retailer { onError(it.message ?: it.toString()) } } else { - onError("Error in logout: Account not found ${account.accountCommon.id} - ${account.username}") + onError( + "Logout: Account not found ${account.accountCommon.id} - ${account.username}") } } } @@ -151,10 +153,11 @@ class Retailer { client(context).resetHistory().addOnSuccessListener { onComplete() }.addOnFailureListener { ex -> - onError(ex.message ?: ex.toString()) + Timberland.e(ex) + onComplete() } }.addOnFailureListener { ex -> - onError(ex.message ?: ex.toString()) + Timberland.e(ex) } } @@ -170,37 +173,37 @@ class Retailer { @OptIn(ExperimentalCoroutinesApi::class) fun orders( context: Context, - onReceipt: (ScanResults) -> Unit, + onReceipt: (ScanResults?) -> Unit, onError: (msg: String) -> Unit, - daysCutOff: Int = 7, + daysCutOff: Int = 15, onComplete: () -> Unit ) { val client: AccountLinkingClient = client(context) var fetchedAccounts = 0 - client.accounts().addOnSuccessListener { mbAccountList -> - if (mbAccountList.isNullOrEmpty()) { - onComplete() - client.close() - }else { - for (retailerAccount in mbAccountList) { - val account = Account.fromRetailerAccount(retailerAccount) - this.orders( - context, - account, - onReceipt, - daysCutOff, - onError - ) { - fetchedAccounts++ - if (fetchedAccounts == mbAccountList.size) { - onComplete() + client.accounts() + .addOnSuccessListener { mbAccountList -> + if (mbAccountList.isNullOrEmpty()) { + onComplete() + client.close() + }else { + for (retailerAccount in mbAccountList) { + val account = Account.fromRetailerAccount(retailerAccount) + this.orders( + context, + account, + onReceipt, + daysCutOff, + ) { + fetchedAccounts++ + if (fetchedAccounts >= mbAccountList.size) { + onComplete() + } } } } } - } .addOnFailureListener { - onError(it.message ?: "Unknown Error in retrieving accounts. $it") + Timberland.e(it) onComplete.invoke() } } @@ -219,26 +222,23 @@ class Retailer { fun orders( context: Context, account: Account, - onScan: (ScanResults) -> Unit, + onScan: (ScanResults?) -> Unit, daysCutOff: Int = 7, - onError: (msg: String) -> Unit, onComplete: (() -> Unit)? = null ) { val client: AccountLinkingClient = client(context, daysCutOff) val id = account.accountCommon.id - val username = account.username val retailerId = RetailerEnum.fromString(id).toMbInt() val ordersSuccessCallback: (Int, ScanResults?, Int, String) -> Unit = { _: Int, results: ScanResults?, remaining: Int, _: String -> - if (results != null) { - onScan(results) - } else { - onError("Null ScanResult in $id - $username. Remaining $remaining") + onScan(results) + if(remaining == 0){ + onComplete?.invoke() } - if (remaining == 0) onComplete?.invoke() } val ordersFailureCallback: (Int, AccountLinkingException) -> Unit = { _: Int, exception: AccountLinkingException -> - onError(exception.message ?: exception.toString()) + Timberland.e(exception) + onComplete?.invoke() } client.orders( retailerId, @@ -325,6 +325,9 @@ class Retailer { client.verify( RetailerEnum.fromString(account.accountCommon.id).value, success = { isVerified: Boolean, _: String -> + activity.findViewById(R.id.webview_container)?.let { + (it.parent as ViewGroup).removeView(it) + } if (isVerified) { account.isVerified = true onVerify?.invoke(account) @@ -338,14 +341,14 @@ class Retailer { } }, failure = { exception -> + activity.findViewById(R.id.webview_container)?.let { + (it.parent as ViewGroup).removeView(it) + } if (exception.code == VERIFICATION_NEEDED && exception.view != null) { exception.view!!.isFocusableInTouchMode = true if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { exception.view!!.focusable = View.FOCUSABLE } - activity.findViewById(R.id.webview_container)?.let { - (it.parent as ViewGroup).removeView(it) - } val viewGroup = (activity.findViewById(android.R.id.content) as ViewGroup).getChildAt(0) as ViewGroup View.inflate(activity, R.layout.webview_container, viewGroup) val webViewContainer = activity.findViewById(R.id.webview_container) diff --git a/example/package-lock.json b/example/package-lock.json index 5e14bd0f..3bba6fcf 100644 --- a/example/package-lock.json +++ b/example/package-lock.json @@ -40,7 +40,7 @@ }, "..": { "name": "@mytiki/capture-receipt-capacitor", - "version": "0.6.2", + "version": "0.7.0", "license": "MIT", "dependencies": { "uuid": "^9.0.1" diff --git a/example/src/app.vue b/example/src/app.vue index 608cedbf..e168dc8e 100644 --- a/example/src/app.vue +++ b/example/src/app.vue @@ -30,7 +30,11 @@ const source = ref() diff --git a/example/src/main.ts b/example/src/main.ts index aa149b34..33aa979a 100644 --- a/example/src/main.ts +++ b/example/src/main.ts @@ -44,8 +44,9 @@ export const scan = async (): Promise => { export const logout = async (): Promise => instance.logout(); export const initialize = async (): Promise => { await instance.initialize( - 'sRwAAAAoY29tLm15dGlraS5zZGsuY2FwdHVyZS5yZWNlaXB0LmNhcGFjaXRvcgY6SQlVDCCrMOCc/jLI1A3BmOhqNvtZLzShMcb3/OLQLiqgWjuHuFiqGfg4fnAiPtRcc5uRJ6bCBRkg8EsKabMQkEsMOuVjvEOejVD497WkMgobMbk/X+bdfhPPGdcAHWn5Vnz86SmGdHX5xs6RgYe5jmJCSLiPmB7cjWmxY5ihkCG12Q==', 'wSNX3mu+YGc/2I1DDd0NmrYHS6zS1BQt2geMUH7DDowER43JGeJRUErOHVwU2tz6xHDXia8BuvXQI3j37I0uYw==', + 'sRwAAAAoY29tLm15dGlraS5zZGsuY2FwdHVyZS5yZWNlaXB0LmNhcGFjaXRvcgY6SQlVDCCrMOCc/jLI1A3BmOhqNvtZLzShMcb3/OLQLiqgWjuHuFiqGfg4fnAiPtRcc5uRJ6bCBRkg8EsKabMQkEsMOuVjvEOejVD497WkMgobMbk/X+bdfhPPGdcAHWn5Vnz86SmGdHX5xs6RgYe5jmJCSLiPmB7cjWmxY5ihkCG12Q==', + 'sRwAAAAoY29tLm15dGlraS5zZGsuY2FwdHVyZS5yZWNlaXB0LmNhcGFjaXRvcgY6SQlVDCCrMOCc/jLI1A3BmOhqNvtZLzShMcb3/OLQLiqgWjuHuFiqGfg4fnAiPtRcc5uRJ6bCBRkg8EsKabMQkEsMOuVjvEOejVD497WkMgobMbk/X+bdfhPPGdcAHWn5Vnz86SmGdHX5xs6RgYe5jmJCSLiPmB7cjWmxY5ihkCG12Q==', ); }; diff --git a/ios/Plugin/Account/AccountTypeEnum.swift b/ios/Plugin/Account/AccountTypeEnum.swift index f9dc1d60..8807e461 100644 --- a/ios/Plugin/Account/AccountTypeEnum.swift +++ b/ios/Plugin/Account/AccountTypeEnum.swift @@ -11,6 +11,6 @@ public enum AccountTypeEnum { case email /// Indicates a retailer account type. case retailer - + /// Indicates a none account type. case none } diff --git a/ios/Plugin/CaptureReceipt.swift b/ios/Plugin/CaptureReceipt.swift index 364ffa2f..072a21e5 100644 --- a/ios/Plugin/CaptureReceipt.swift +++ b/ios/Plugin/CaptureReceipt.swift @@ -19,7 +19,9 @@ public class CaptureReceipt: NSObject { /// Initializes the ReceiptCapture class. /// - /// - Parameter call: The CAPPluginCall representing the initialization request. + /// - Parameters: + /// - licenseKey: The license key for BlinkReceipt. + /// - productKey: The product key for BlinkReceipt. public func initialize(licenseKey: String, productKey: String) { let scanManager = BRScanManager.shared() scanManager.licenseKey = licenseKey @@ -30,7 +32,10 @@ public class CaptureReceipt: NSObject { /// Handles user login for receipt management. /// - /// - Parameter call: The CAPPluginCall representing the login request. + /// - Parameters: + /// - account: An instance of the Account struct containing user and account information. + /// - onError: A closure to handle error messages. + /// - onComplete: A closure to handle the completion of the login process. public func login(account: Account, onError: @escaping (String) -> Void, onComplete: @escaping (Account) -> Void) { DispatchQueue.main.async { switch account.accountType.type { @@ -53,9 +58,13 @@ public class CaptureReceipt: NSObject { } } } + /// Handles user logout from receipt management. /// - /// - Parameter call: The CAPPluginCall representing the logout request. + /// - Parameters: + /// - onError: A closure to handle error messages. + /// - onComplete: A closure to handle the completion of the logout process. + /// - account: An optional instance of the Account struct containing user and account information. public func logout(onError: @escaping (String) -> Void, onComplete: @escaping () -> Void, account: Account? = nil) { if(account != nil){ switch(account?.accountType.type){ @@ -73,9 +82,13 @@ public class CaptureReceipt: NSObject { retailer!.logout(onError: {error in onError(error)}, onComplete: {onComplete()}) } } + /// Retrieves a list of user accounts for receipt management. - /// - /// - Parameter call: The CAPPluginCall representing the request for account information. + /// + /// - Parameters: + /// - onError: A closure to handle error messages. + /// - onComplete: A closure to handle the completion of the account retrieval process. + /// - onAccount: A closure to handle individual account information. public func accounts( onError: @escaping (String) -> Void, onComplete: @escaping () -> Void, onAccount: (Account) -> Void) { guard let retailer = retailer else { onError("Retailer not initialized. Did you call .initialize()?") @@ -94,7 +107,10 @@ public class CaptureReceipt: NSObject { /// Initiates receipt scanning based on the specified account type. /// - /// - Parameter call: The CAPPluginCall representing the scan request. + /// - Parameters: + /// - onError: A closure to handle error messages. + /// - onReceipt: A closure to handle individual receipt results. + /// - onComplete: A closure to handle the completion of the scanning process. public func scan(onError: @escaping (String) -> Void, onReceipt: @escaping (BRScanResults) -> Void, onComplete: @escaping () -> Void) { guard let retailer = retailer else { onError("Retailer not initialized. Did you call .initialize()?") diff --git a/ios/Plugin/Email/Email.swift b/ios/Plugin/Email/Email.swift index 0383cfab..33552ecd 100644 --- a/ios/Plugin/Email/Email.swift +++ b/ios/Plugin/Email/Email.swift @@ -30,8 +30,9 @@ public class Email { /// Logs in a user account using the provided credentials or initiates OAuth authentication for Gmail. /// /// - Parameters: - /// - account: An instance of the Account struct containing user and account information. - /// - pluginCall: The CAPPluginCall object representing the plugin call. + /// - account: An instance of the Account class containing user and account information. + /// - onError: A closure to handle error messages. + /// - onSuccess: A closure to handle success actions. public func login(_ account: Account, onError: @escaping (String) -> Void, onSuccess: @escaping () -> Void) { let email = BRIMAPAccount(provider: .gmailIMAP, email: account.user, password: account.password!) DispatchQueue.main.async { @@ -52,7 +53,8 @@ public class Email { /// Logs out a user account or signs out of all accounts. /// /// - Parameters: - /// - pluginCall: The CAPPluginCall object representing the plugin call. + /// - onError: A closure to handle error messages. + /// - onComplete: A closure to handle completion actions. /// - account: An optional instance of the Account struct containing user and account information. public func logout(onError: @escaping (String) -> Void, onComplete: @escaping () -> Void, account: Account? = nil){ if(account != nil){ @@ -73,17 +75,19 @@ public class Email { onError(error.debugDescription) }else{ BREReceiptManager.shared().resetEmailsChecked() + self.defaults.set(Date.distantPast, forKey: "lastIMAPScan") onComplete() } }) } } - /// Retrieves e-receipts for a user account or initiates OAuth authentication for scanning OAuth. + /// Retrieves e-receipts for a user email account /// /// - Parameters: - /// - pluginCall: The CAPPluginCall object representing the plugin call. - /// - account: An optional instance of the Account struct containing user and account information. + /// - onError: A closure to handle error messages. + /// - onReceipt: A closure to handle individual receipt results. + /// - onComplete: A closure to handle completion actions. public func scan(onError: @escaping (String) -> Void, onReceipt: @escaping (BRScanResults) -> Void, onComplete: @escaping () -> Void) { BREReceiptManager.shared().dayCutoff = getDayCutOff() Task(priority: .high){ @@ -101,7 +105,10 @@ public class Email { /// Retrieves a list of linked email accounts. /// - /// - Returns: An array of Account objects representing linked email accounts. + /// - Parameters: + /// - onError: A closure to handle error messages. + /// - onAccount: A closure to handle individual linked email accounts. + /// - onComplete: A closure to handle completion actions. public func accounts(onError: (String) -> Void, onAccount: (Account) -> Void, onComplete: () -> Void) { let linkedAccounts = BREReceiptManager.shared().getLinkedAccounts() linkedAccounts?.forEach{ brAccount in @@ -111,13 +118,14 @@ public class Email { } onComplete() } + private func getDayCutOff() -> Int{ if (defaults.object(forKey: "lastIMAPScan") != nil) { let dayCutOffSaved = defaults.object(forKey: "lastIMAPScan") as! Date let timeInterval = dayCutOffSaved.timeIntervalSinceNow let difference = Int((timeInterval)) / 86400 - if(difference < 15){ + if(difference < 15 && difference >= 0){ return difference } } diff --git a/ios/Plugin/Plugin/CaptureReceiptPlugin.swift b/ios/Plugin/Plugin/CaptureReceiptPlugin.swift index aefdec78..6bfbc282 100644 --- a/ios/Plugin/Plugin/CaptureReceiptPlugin.swift +++ b/ios/Plugin/Plugin/CaptureReceiptPlugin.swift @@ -1,8 +1,10 @@ /* + * ReceiptCapturePlugin Class * Copyright (c) TIKI Inc. * MIT license. See LICENSE file in root directory. */ +/// A Swift class representing a Capacitor plugin for receipt capture and management. import Foundation import Capacitor import BlinkReceipt @@ -13,12 +15,18 @@ public class CaptureReceiptPlugin: CAPPlugin { private let receiptCapture = CaptureReceipt() + /// Initializes the plugin with the provided license and product keys. + /// + /// - Parameter call: The CAPPluginCall representing the initialization request. @objc public func initialize(_ call: CAPPluginCall) { let reqInitialize = try! ReqInitialize(call) receiptCapture.initialize(licenseKey: reqInitialize.licenseKey, productKey: reqInitialize.productKey) call.resolve() } + /// Handles user lgoin from receipt management. + /// + /// - Parameter call: The CAPPluginCall representing the logout request. @objc public func login(_ call: CAPPluginCall) { let reqAccount = try! ReqAccount(call) receiptCapture.login(account: reqAccount.account(), @@ -26,6 +34,9 @@ public class CaptureReceiptPlugin: CAPPlugin { onComplete: { account in call.resolve( (account.toResultData()) )} ) } + /// Handles user logout from receipt management. + /// + /// - Parameter call: The CAPPluginCall representing the logout request. @objc func logout(_ call: CAPPluginCall) { let reqAccount = try? ReqAccount(call) if(reqAccount == nil){ @@ -40,6 +51,10 @@ public class CaptureReceiptPlugin: CAPPlugin { } } + + /// Retrieves a list of accounts for receipt management. + /// + /// - Parameter call: The CAPPluginCall representing the request for account information. @objc func accounts(_ call: CAPPluginCall){ let req = try! Req(call) receiptCapture.accounts( @@ -48,6 +63,9 @@ public class CaptureReceiptPlugin: CAPPlugin { onAccount: {account in onAccount(requestId: req.requestId, account: account)}) } + /// Initiates e-receipt scanning. + /// + /// - Parameter call: The CAPPluginCall representing the scan request. @objc func scan(_ call: CAPPluginCall) { let req = try! Req(call) receiptCapture.scan(onError: {error in self.onError(req.requestId, error)}, @@ -55,21 +73,41 @@ public class CaptureReceiptPlugin: CAPPlugin { onComplete: {}) } + // MARK: - Private Helper Methods + + /// Notifies the listeners with receipt scan results. + /// + /// - Parameters: + /// - requestId: The unique identifier for the request. + /// - scanResults: The results of the receipt scan. private func onReceipt(_ requestId: String, _ scanResults: BRScanResults){ let rsp = RspReceipt(requestId: requestId, scanResults: scanResults).toPluginCallResultData() self.notifyListeners("onCapturePluginResult", data: rsp) } + /// Notifies the listeners about an error. + /// + /// - Parameters: + /// - requestId: The unique identifier for the request. + /// - message: The error message. private func onError(_ requestId: String, _ message: String){ let rsp = RspError(requestId: requestId, message: message).toPluginCallResultData() self.notifyListeners("onCapturePluginResult", data: rsp) } + /// Notifies the listeners about the completion of an operation. + /// + /// - Parameter requestId: The unique identifier for the request. private func onComplete(requestId: String){ let rsp = Rsp(requestId: requestId, event: .onComplete).toPluginCallResultData() self.notifyListeners("onCapturePluginResult", data: rsp) } + /// Notifies the listeners with account information. + /// + /// - Parameters: + /// - requestId: The unique identifier for the request. + /// - account: The account information. private func onAccount(requestId: String, account: Account) { let rsp = RspAccount(requestId: requestId, account: account).toPluginCallResultData() self.notifyListeners("onCapturePluginResult", data: rsp) diff --git a/ios/Plugin/Plugin/Req/Req.swift b/ios/Plugin/Plugin/Req/Req.swift index 340b49c5..5d912277 100644 --- a/ios/Plugin/Plugin/Req/Req.swift +++ b/ios/Plugin/Plugin/Req/Req.swift @@ -1,4 +1,5 @@ /* + * Req Class * Copyright (c) TIKI Inc. * MIT license. See LICENSE file in the root directory. */ @@ -6,9 +7,15 @@ import Foundation import Capacitor +/// A Swift class for handling request data in a Capacitor plugin. public class Req { let requestId: String + + /// Initializes the Req class with the request identifier from a CAPPluginCall. + /// + /// - Parameter call: The CAPPluginCall object representing the request. + /// - Throws: An error if the request identifier is missing. init(_ call: CAPPluginCall) throws { guard let reqId = call.getString("requestId") else { call.reject("Add a requestId in the call.") diff --git a/ios/Plugin/Plugin/Req/ReqAccount.swift b/ios/Plugin/Plugin/Req/ReqAccount.swift index 131f0786..22cddd00 100644 --- a/ios/Plugin/Plugin/Req/ReqAccount.swift +++ b/ios/Plugin/Plugin/Req/ReqAccount.swift @@ -1,4 +1,5 @@ /* + * ReqAccount Class * Copyright (c) TIKI Inc. * MIT license. See LICENSE file in the root directory. */ @@ -6,12 +7,21 @@ import Foundation import Capacitor +/// A Swift class for handling account-related request data in a Capacitor plugin. public class ReqAccount : Req { + /// The common account information. let accountCommon: AccountCommon + /// The username associated with the account. let username: String + /// The password associated with the account (optional). let password: String? + /// Whether the account is verified (optional). let isVerified: Bool? + /// Initializes the ReqAccount class with data from a CAPPluginCall. + /// + /// - Parameter call: The CAPPluginCall object representing the request. + /// - Throws: An error if required data is missing. override init(_ call: CAPPluginCall) throws { if(AccountCommon.defaults[call.getString("id") ?? ""] == nil){ accountCommon = AccountCommon(type: .none, source: "") @@ -24,6 +34,9 @@ public class ReqAccount : Req { try super.init(call) } + /// Constructs an Account object using the gathered data. + /// + /// - Returns: An Account object created from the collected data. public func account() -> Account{ return Account(accountType: self.accountCommon, user: self.username, password: self.password, isVerified: self.isVerified) } diff --git "a/ios/Plugin/Plugin/Req/ReqInitialize\342\200\216.swift" "b/ios/Plugin/Plugin/Req/ReqInitialize\342\200\216.swift" index c4dd5c56..a1d6157f 100644 --- "a/ios/Plugin/Plugin/Req/ReqInitialize\342\200\216.swift" +++ "b/ios/Plugin/Plugin/Req/ReqInitialize\342\200\216.swift" @@ -1,4 +1,5 @@ /* + * ReqInitialize Class * Copyright (c) TIKI Inc. * MIT license. See LICENSE file in the root directory. */ @@ -6,7 +7,8 @@ import Foundation import Capacitor -/// A structure representing the initialization request parameters for the ReceiptCapture plugin. + +/// A class representing the initialization request parameters for the ReceiptCapture plugin. public class ReqInitialize : Req{ /// The license key used for authentication. @@ -15,12 +17,28 @@ public class ReqInitialize : Req{ /// The product key associated with the ReceiptCapture plugin. var productKey: String + /// Initializes the ReqInitialize class with data from a CAPPluginCall. + /// + /// - Parameter call: The CAPPluginCall object representing the request. + /// - Throws: An error if required data is missing. override init(_ call: CAPPluginCall) throws { - if(call.getString("productKey") == nil || call.getString("licenseKey") == nil ){ - call.reject("Please, provide a valid LicenseKey and ProductKey") - throw NSError() + if(call.getString("productKey") == nil){ + call.reject("Please, provide a valid ProductKey") + throw NSError( + domain: "tiki", + code: 400, + userInfo:["message": "Provide a Product Intelligence Key for initialization."] + ) + } + if(call.getString("ios") == nil ){ + call.reject("Please, provide a valid iOS License key") + throw NSError( + domain: "tiki", + code: 400, + userInfo:["message": "Provide an iOS License Key for initialization."] + ) } - licenseKey = call.getString("licenseKey")! + licenseKey = call.getString("ios")! productKey = call.getString("productKey")! try super.init(call) } diff --git a/ios/Plugin/Plugin/Rsp/Rsp.swift b/ios/Plugin/Plugin/Rsp/Rsp.swift index 8e44c2d3..4df94001 100644 --- a/ios/Plugin/Plugin/Rsp/Rsp.swift +++ b/ios/Plugin/Plugin/Rsp/Rsp.swift @@ -1,4 +1,5 @@ /* + * Rsp Class * Copyright (c) TIKI Inc. * MIT license. See LICENSE file in the root directory. */ @@ -7,15 +8,28 @@ import Foundation import Capacitor import Foundation +/// A class representing a response structure for a Capacitor plugin. public class Rsp { + /// The unique identifier for the request associated with this response. let requestId: String + + /// The event type associated with this callback response event. let event: PluginEvent + + /// Initializes the Rsp class with the provided request identifier and event. + /// + /// - Parameters: + /// - requestId: The unique identifier for the associated request. + /// - event: The event type associated with this response. init(requestId: String, event: PluginEvent) { self.requestId = requestId self.event = event } + /// Converts the response data to a format suitable for a Capacitor plugin call result. + /// + /// - Returns: A dictionary containing the response data. func toPluginCallResultData() -> [String: Any] { return [ "requestId": requestId, diff --git a/ios/Plugin/Plugin/Rsp/RspAccount.swift b/ios/Plugin/Plugin/Rsp/RspAccount.swift index 52f62f73..5a0e8dff 100644 --- a/ios/Plugin/Plugin/Rsp/RspAccount.swift +++ b/ios/Plugin/Plugin/Rsp/RspAccount.swift @@ -1,16 +1,9 @@ /* + * RspAccount Class * Copyright (c) TIKI Inc. * MIT license. See LICENSE file in the root directory. */ -/** - A struct representing an account's response data for the ReceiptCapture plugin. - - This struct is used to encapsulate account information for use in plugin responses. It contains details such as the username, source, and verification status of an account. - - - Note: This struct is typically used to construct response data for account-related plugin calls. - */ - import Foundation import Capacitor @@ -29,6 +22,7 @@ public class RspAccount : Rsp{ /** Initializes an `RspAccount` object with the provided account details. + - Parameter requestId: The unique identifier for the associated request. - Parameter account: An `Account` object containing the account information. */ public init(requestId: String, account: Account) { diff --git a/ios/Plugin/Plugin/Rsp/RspError.swift b/ios/Plugin/Plugin/Rsp/RspError.swift index 2fa95417..a7d93732 100644 --- a/ios/Plugin/Plugin/Rsp/RspError.swift +++ b/ios/Plugin/Plugin/Rsp/RspError.swift @@ -1,19 +1,41 @@ /* + * RspError Class * Copyright (c) TIKI Inc. * MIT license. See LICENSE file in the root directory. */ +/** + A class representing an error response for the ReceiptCapture plugin. + + This class is used to encapsulate error information for use in plugin responses. It contains details such as an error message and an error code enumeration. + + - Note: This class is typically used to construct response data for error-related plugin calls. + */ public class RspError: Rsp { + /// The error message associated with the response. let message: String + + /// The error code associated with the response. let code: RspErrorEnum + /** + Initializes an `RspError` object with the provided error details. + + - Parameter requestId: The unique identifier for the associated request. + - Parameter message: The error message describing the issue. + - Parameter code: The error code enumeration (default is `.ERROR`). + */ init(requestId: String, message: String, code: RspErrorEnum = .ERROR) { self.message = message self.code = code super.init(requestId: requestId, event: PluginEvent.onError) } - // Converts the RSP error data to a dictionary + /** + Converts the `RspError` object into a dictionary suitable for use in plugin response data. + + - Returns: A dictionary representing the error data in a format suitable for a Capacitor plugin call result. + */ override func toPluginCallResultData() -> [String: Any] { var ret = super.toPluginCallResultData() ret["payload"] = [ diff --git a/ios/Plugin/Plugin/Rsp/RspReceipt.swift b/ios/Plugin/Plugin/Rsp/RspReceipt.swift index 162bb8cb..57f9d2e0 100644 --- a/ios/Plugin/Plugin/Rsp/RspReceipt.swift +++ b/ios/Plugin/Plugin/Rsp/RspReceipt.swift @@ -1,4 +1,5 @@ /* + * RspReceipt Class * Copyright (c) TIKI Inc. * MIT license. See LICENSE file in the root directory. */ @@ -9,9 +10,9 @@ import BlinkEReceipt import Capacitor /** - Represents a response containing receipt information. - - This struct is used to convey details about a receipt, including various receipt fields, such as date, time, products, coupons, totals, and more. + A class representing a response containing receipt information for the ReceiptCapture plugin. + + This class encapsulates detailed information about a receipt, including various receipt fields such as date, time, products, coupons, totals, and more. */ public class RspReceipt : Rsp{ /// The date of the receipt, if available. diff --git a/ios/Plugin/Retailer/Retailer.swift b/ios/Plugin/Retailer/Retailer.swift index 1bae1622..462ba4e2 100644 --- a/ios/Plugin/Retailer/Retailer.swift +++ b/ios/Plugin/Retailer/Retailer.swift @@ -33,9 +33,10 @@ public class Retailer : CAPPlugin{ /// /// - Parameters: /// - account: An instance of the Account struct containing user and account information. - /// - call: The CAPPluginCall object representing the plugin call. + /// - onError: A closure to handle error messages. + /// - onSuccess: A closure to handle successful login. public func login(_ account: Account, onError: @escaping (String) -> Void, onSuccess: @escaping (Account) -> Void) { - let dayCutoff: Int = 7 + let dayCutoff: Int = 15 let username: String = account.user guard let retailer: BRAccountLinkingRetailer = RetailerEnum(rawValue: account.accountType.source)?.toBRAccountLinkingRetailer() else { @@ -54,7 +55,7 @@ public class Retailer : CAPPlugin{ let error = BRAccountLinkingManager.shared().linkRetailer(with: connection) if (error == .none) { // Success - Task(priority: .high){ + DispatchQueue.main.async{ BRAccountLinkingManager.shared().verifyRetailer(with: connection, withCompletion: { error, viewController, sessionId in self.verifyRetailerCallback(error,viewController, connection, { error in onError(error) }, {account in onSuccess(account)}, account) @@ -67,12 +68,16 @@ public class Retailer : CAPPlugin{ /// Logs out a user account. /// /// - Parameters: - /// - call: The CAPPluginCall object representing the plugin call. + /// - onError: A closure to handle error messages. + /// - onComplete: A closure to handle the completion of the logout process. /// - account: An instance of the Account struct containing user and account information. public func logout(onError: @escaping (String) -> Void, onComplete: @escaping () -> Void, account: Account? = nil) { if (account == nil) { - BRAccountLinkingManager.shared().unlinkAllAccounts { - onComplete() + BRAccountLinkingManager.shared().resetHistory() + DispatchQueue.main.async { + BRAccountLinkingManager.shared().unlinkAllAccounts { + onComplete() + } } return } @@ -81,15 +86,21 @@ public class Retailer : CAPPlugin{ onError("Unsuported retailer \(account!.accountType.type)") return } - BRAccountLinkingManager.shared().unlinkAccount(for: retailer) { + + BRAccountLinkingManager.shared().resetHistory(for: retailer) + DispatchQueue.main.async { + BRAccountLinkingManager.shared().unlinkAccount(for: retailer) { onComplete() + } } + } /// Retrieves orders for a specific user account or for all linked accounts. /// /// - Parameters: - /// - account: An optional instance of the Account struct containing user and account information. - /// - call: The CAPPluginCall object representing the plugin call. + /// - onError: A closure to handle error messages. + /// - onReceipt: A closure to handle individual receipt results. + /// - onComplete: A closure to handle the completion of the order retrieval process. public func orders(onError: @escaping (String) -> Void, onReceipt: @escaping(BRScanResults) -> Void, onComplete: @escaping () -> Void){ Task(priority: .high) { let retailers = BRAccountLinkingManager.shared().getLinkedRetailers() @@ -101,9 +112,15 @@ public class Retailer : CAPPlugin{ BRAccountLinkingManager.shared().grabNewOrders( for: retailerId) { retailer, order, remaining, viewController, errorCode, sessionId in if(errorCode == .none && order != nil){ onReceipt(order!) + print(order!) + } + if(remaining == 0){ + onComplete() } + } } + } } } @@ -112,7 +129,10 @@ public class Retailer : CAPPlugin{ /// Retrieves a list of linked accounts. /// - /// - Returns: An array of Account objects representing linked accounts. + /// - Parameters: + /// - onError: A closure to handle error messages. + /// - onAccount: A closure to handle individual linked email accounts. + /// - onComplete: A closure to handle the completion of the account retrieval process. public func accounts (onError: (String) -> Void, onAccount: (Account) -> Void, onComplete: () -> Void) { let retailers = BRAccountLinkingManager.shared().getLinkedRetailers() for ret in retailers { @@ -126,12 +146,13 @@ public class Retailer : CAPPlugin{ } /// Handles the callback after attempting to verify a retailer account. /// - /// - Parameters: - /// - error: The BRAccountLinkingError code indicating the result of the verification. - /// - viewController: The UIViewController to present for verification if needed. - /// - connection: The BRAccountLinkingConnection object representing the user's account connection. - /// - call: The CAPPluginCall object representing the plugin call. - /// - account: An instance of the Account struct containing user and account information. + /// - Parameters: + /// - error: The BRAccountLinkingError code indicating the result of the verification. + /// - viewController: The UIViewController to present for verification if needed. + /// - connection: The BRAccountLinkingConnection object representing the user's account connection. + /// - onError: A closure to handle error messages. + /// - onComplete: A closure to handle the completion of the verification process. + /// - account: An instance of the Account struct containing user and account information. private func verifyRetailerCallback( _ error: BRAccountLinkingError, _ viewController: UIViewController?, diff --git a/package-lock.json b/package-lock.json index 1f080749..ee8cd025 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@mytiki/capture-receipt-capacitor", - "version": "0.6.2", + "version": "0.7.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@mytiki/capture-receipt-capacitor", - "version": "0.6.2", + "version": "0.7.0", "license": "MIT", "dependencies": { "uuid": "^9.0.1" diff --git a/package.json b/package.json index aa313c7e..22dbeefc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@mytiki/capture-receipt-capacitor", - "version": "0.6.2", + "version": "0.7.0", "description": "Capture receipts w/ TIKI!", "main": "dist/index.cjs.js", "module": "dist/index.es.js", diff --git a/src/callback/callback-mgr.ts b/src/callback/callback-mgr.ts index b11f38d6..ced71bfb 100644 --- a/src/callback/callback-mgr.ts +++ b/src/callback/callback-mgr.ts @@ -73,10 +73,10 @@ export class CallbackManager { case CallbackEvent.onComplete: { const callback = registered.callback as CompleteCallback; callback(); - const requestId = body.requestId - const callbackEvents = Object.keys(CallbackEvent) + const requestId = body.requestId; + const callbackEvents = Object.keys(CallbackEvent); for (const event of callbackEvents) { - this.remove(`${event}:${requestId}`) + this.remove(`${event}:${requestId}`); } break; } diff --git a/src/capture-receipt.ts b/src/capture-receipt.ts index 619f8890..e2347560 100644 --- a/src/capture-receipt.ts +++ b/src/capture-receipt.ts @@ -30,13 +30,21 @@ export class CaptureReceipt { } /** - * Initializes the SDK - * @param licenseKey The license key of the package. - * @param productKey The product intelligence key of the package. + * Initializes the SDK. + * + * One of (or both) ios or android is required depending on the platform used. + * + * @param product The product intelligence key for the package. + * @param ios The iOS license key. + * @param android The Android license key. * @returns A Promise that resolves to void on completion */ - initialize(licenseKey: string, productKey: string): Promise { - const req: ReqInitialize = new ReqInitialize(licenseKey, productKey); + initialize( + productKey: string, + ios: string | undefined = undefined, + android: string | undefined = undefined, + ): Promise { + const req: ReqInitialize = new ReqInitialize(productKey, ios, android); return this.plugin.initialize(req); } diff --git a/src/plugin/req/req-initialize.ts b/src/plugin/req/req-initialize.ts index 335cba66..70006e92 100644 --- a/src/plugin/req/req-initialize.ts +++ b/src/plugin/req/req-initialize.ts @@ -12,12 +12,14 @@ import type { Req } from './req'; */ export class ReqInitialize implements Req { requestId: string; - licenseKey: string; + ios?: string; + android?: string; productKey: string; - constructor(licenseKey: string, productKey: string) { + constructor(productKey: string, ios: string | undefined = undefined, android: string | undefined = undefined) { this.requestId = uuid.v4(); - this.licenseKey = licenseKey; this.productKey = productKey; + this.ios = ios; + this.android = android; } }