diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml
index fe32683..ba14347 100644
--- a/.github/workflows/deploy.yaml
+++ b/.github/workflows/deploy.yaml
@@ -35,8 +35,14 @@ jobs:
echo "should_publish=true" >> $GITHUB_OUTPUT
fi
+ # Publishing is gated behind the ENABLE_PUBLISH repo variable so the
+ # Deploy workflow still runs and succeeds (which is what sync.yaml's
+ # workflow_run trigger needs) while we test repo sync + branding.
+ # To start publishing for real: set repo variable ENABLE_PUBLISH=true.
- name: Publish to pub.dev
- if: steps.version_check.outputs.should_publish == 'true'
+ if: >-
+ vars.ENABLE_PUBLISH == 'true' &&
+ steps.version_check.outputs.should_publish == 'true'
run: |
# Создаём файл с credentials (берём из секретов)
echo "${{ secrets.PUB_DEV_CREDENTIALS }}" > ~/.pub-cache/credentials.json
diff --git a/.github/workflows/sync.yaml b/.github/workflows/sync.yaml
new file mode 100644
index 0000000..b9c5033
--- /dev/null
+++ b/.github/workflows/sync.yaml
@@ -0,0 +1,40 @@
+name: Repository synchronization
+
+on:
+ workflow_run:
+ workflows:
+ - Deploy
+ types:
+ - completed
+ workflow_dispatch:
+
+jobs:
+ prepare:
+ if: |
+ (github.event_name == 'workflow_dispatch' ||
+ github.event.workflow_run.conclusion == 'success') &&
+ !contains(github.repository_owner, 'personaclick')
+ runs-on: ubuntu-latest
+ outputs:
+ replacements: ${{ steps.getReplacementsStep.outputs.replacements }}
+ steps:
+ - uses: rees46/workflow/.github/actions/sync/read-replacements@master
+ id: getReplacementsStep
+ with:
+ appId: ${{ vars.PUBLIVERSIONER_ID }}
+ appSecret: ${{ secrets.PUBLIVERSIONER_SECRET }}
+ replacementsRepo: rees46/workflow
+ repositoryOwner: rees46
+ replacementsPath: github/repo-sync-replacements/flutter-sdk.yml
+
+ repoSync:
+ needs: prepare
+ uses: rees46/workflow/.github/workflows/reusable-repo-sync.yml@master
+ secrets:
+ appSecret: ${{ secrets.PERSONACLICK_COURIER_SECRET }}
+ with:
+ appId: ${{ vars.PERSONACLICK_COURIER_ID }}
+ replacements: ${{ needs.prepare.outputs.replacements }}
+ repositoryOwner: personaclick
+ targetRepository: personaclick/flutter-sdk
+ reviewerUsername: ${{ vars.REVIEWER_USERNAME }}
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..b9d7f25
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,33 @@
+# Miscellaneous
+*.class
+*.log
+*.pyc
+*.swp
+.DS_Store
+.atom/
+.build/
+.buildlog/
+.history
+.svn/
+.swiftpm/
+migrate_working_dir/
+
+# IntelliJ related
+*.iml
+*.ipr
+*.iws
+.idea/
+
+# The .vscode folder contains launch configuration and tasks you configure in
+# VS Code which you may wish to be included in version control, so this line
+# is commented out by default.
+#.vscode/
+
+# Flutter/Dart/Pub related
+# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
+/pubspec.lock
+**/doc/api/
+.dart_tool/
+.flutter-plugins-dependencies
+/build/
+/coverage/
diff --git a/.metadata b/.metadata
new file mode 100644
index 0000000..0798d44
--- /dev/null
+++ b/.metadata
@@ -0,0 +1,33 @@
+# This file tracks properties of this Flutter project.
+# Used by Flutter tool to assess capabilities and perform upgrades etc.
+#
+# This file should be version controlled and should not be manually edited.
+
+version:
+ revision: "ff37bef603469fb030f2b72995ab929ccfc227f0"
+ channel: "stable"
+
+project_type: plugin
+
+# Tracks metadata for the flutter migrate command
+migration:
+ platforms:
+ - platform: root
+ create_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
+ base_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
+ - platform: android
+ create_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
+ base_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
+ - platform: ios
+ create_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
+ base_revision: ff37bef603469fb030f2b72995ab929ccfc227f0
+
+ # User provided section
+
+ # List of Local paths (relative to this file) that should be
+ # ignored by the migrate tool.
+ #
+ # Files that are not part of the templates will be ignored by default.
+ unmanaged_files:
+ - 'lib/main.dart'
+ - 'ios/Runner.xcodeproj/project.pbxproj'
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..41cc7d8
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,3 @@
+## 0.0.1
+
+* TODO: Describe initial release.
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..ba75c69
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1 @@
+TODO: Add your license here.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..258531a
--- /dev/null
+++ b/README.md
@@ -0,0 +1,60 @@
+# rees46_flutter_sdk
+
+Flutter plugin wrapper around REES46 native SDKs (Android/iOS).
+
+## Getting Started
+
+### Install
+
+Add dependency in your app:
+
+```yaml
+dependencies:
+ personalization_flutter_sdk: ^0.0.1
+```
+
+### Initialize
+
+```dart
+import 'package:personaclick_flutter_sdk/personaclick_flutter_sdk.dart';
+
+final sdk = Rees46FlutterSdk();
+
+await sdk.initialize(
+ const Rees46InitConfig(
+ shopId: 'YOUR_SHOP_ID',
+ apiDomain: 'api.rees46.ru',
+ // stream defaults to 'ios' on iOS and 'android' on Android if omitted.
+ stream: 'ios',
+ enableLogs: false,
+ autoSendPushToken: true,
+ sendAdvertisingId: false,
+ enableAutoPopupPresentation: true,
+ needReInitialization: false,
+ ),
+);
+```
+
+### API structure (core + wrappers)
+
+- **Core (private-ish)**: `PersonalizationSdk` and `SdkInitConfig` live in `lib/src/`.
+- **REES46 wrapper**: `Rees46FlutterSdk` + `Rees46InitConfig` in `lib/rees46_flutter_sdk.dart`.
+- **PersonaClick wrapper**: `PersonaclickFlutterSdk` + `PersonaclickInitConfig` in `lib/personaclick_flutter_sdk.dart` (primary entrypoint in PersonaClick repo).
+
+### Run demo app
+
+```bash
+cd example
+fvm flutter run
+```
+
+### Notes
+
+- **Android**: uses Maven dependency `com.rees46:rees46-sdk:2.28.0` and calls `SDK.initialize(...)`. Some iOS-only init flags are accepted by Dart API but ignored on Android.
+- **iOS**: uses CocoaPods dependency `REES46 (3.23.0)` and calls `createPersonalizationSDK(...)`.
+- **Pushes**:
+ - **Android**: when `autoSendPushToken=true`, the native SDK fetches the FCM token via `FirebaseMessaging.getInstance().token` during initialization and sends it.
+ - **iOS**: when `autoSendPushToken=true`, the native SDK requests notification permission and registers for remote notifications. The Flutter plugin also forwards `didRegisterForRemoteNotificationsWithDeviceToken` and `didReceiveRemoteNotification` AppDelegate callbacks to the native SDK.
+
+For Flutter plugin development basics, see Flutter docs: [develop plugins](https://flutter.dev/to/develop-plugins).
+
diff --git a/analysis_options.yaml b/analysis_options.yaml
new file mode 100644
index 0000000..a5744c1
--- /dev/null
+++ b/analysis_options.yaml
@@ -0,0 +1,4 @@
+include: package:flutter_lints/flutter.yaml
+
+# Additional information about this file can be found at
+# https://dart.dev/guides/language/analysis-options
diff --git a/android/.gitignore b/android/.gitignore
new file mode 100644
index 0000000..161bdcd
--- /dev/null
+++ b/android/.gitignore
@@ -0,0 +1,9 @@
+*.iml
+.gradle
+/local.properties
+/.idea/workspace.xml
+/.idea/libraries
+.DS_Store
+/build
+/captures
+.cxx
diff --git a/android/build.gradle.kts b/android/build.gradle.kts
new file mode 100644
index 0000000..dd4091d
--- /dev/null
+++ b/android/build.gradle.kts
@@ -0,0 +1,95 @@
+group = "com.rees46.rees46_flutter_sdk"
+version = "1.0-SNAPSHOT"
+
+buildscript {
+ val kotlinVersion = "2.2.20"
+ repositories {
+ google()
+ mavenCentral()
+ maven(url = "https://jitpack.io")
+ }
+
+ dependencies {
+ classpath("com.android.tools.build:gradle:8.11.1")
+ classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion")
+ }
+}
+
+allprojects {
+ repositories {
+ google()
+ mavenCentral()
+ maven(url = "https://jitpack.io")
+ }
+}
+
+plugins {
+ id("com.android.library")
+ id("kotlin-android")
+}
+
+android {
+ namespace = "com.rees46.rees46_flutter_sdk"
+
+ compileSdk = 36
+
+ flavorDimensions += "brand"
+
+ productFlavors {
+ create("rees46") {
+ dimension = "brand"
+ }
+ }
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+
+ kotlinOptions {
+ jvmTarget = JavaVersion.VERSION_17.toString()
+ }
+
+ sourceSets {
+ getByName("main") {
+ java.srcDirs("src/main/kotlin")
+ }
+ getByName("test") {
+ java.srcDirs("src/test/kotlin")
+ }
+ }
+
+ defaultConfig {
+ minSdk = 24
+ }
+
+ testOptions {
+ unitTests {
+ isIncludeAndroidResources = true
+ all {
+ it.useJUnitPlatform()
+
+ it.outputs.upToDateWhen { false }
+
+ it.testLogging {
+ events("passed", "skipped", "failed", "standardOut", "standardError")
+ showStandardStreams = true
+ }
+ }
+ }
+ }
+}
+
+dependencies {
+ // REES46 Android SDK (Maven Central).
+ //
+ // Note: the 2.x line in Maven Central uses versions like 2.6.0 (not 2.28.0).
+ val rees46AndroidSdkVersion = "2.6.0"
+ add(
+ "rees46Implementation",
+ "com.rees46:rees46-sdk:$rees46AndroidSdkVersion",
+ )
+
+ testImplementation("org.jetbrains.kotlin:kotlin-test")
+ testImplementation("org.mockito:mockito-core:5.0.0")
+}
diff --git a/android/settings.gradle b/android/settings.gradle
new file mode 100644
index 0000000..afbb760
--- /dev/null
+++ b/android/settings.gradle
@@ -0,0 +1 @@
+rootProject.name = 'rees46_flutter_sdk'
diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts
new file mode 100644
index 0000000..afbb760
--- /dev/null
+++ b/android/settings.gradle.kts
@@ -0,0 +1 @@
+rootProject.name = 'rees46_flutter_sdk'
diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..c312d8b
--- /dev/null
+++ b/android/src/main/AndroidManifest.xml
@@ -0,0 +1,3 @@
+
+
diff --git a/android/src/main/kotlin/com/rees46/rees46_flutter_sdk/FlutterTrackingBridge.kt b/android/src/main/kotlin/com/rees46/rees46_flutter_sdk/FlutterTrackingBridge.kt
new file mode 100644
index 0000000..893831c
--- /dev/null
+++ b/android/src/main/kotlin/com/rees46/rees46_flutter_sdk/FlutterTrackingBridge.kt
@@ -0,0 +1,444 @@
+package com.rees46.rees46_flutter_sdk
+
+import com.personalization.SDK
+import com.personalization.api.OnApiCallbackListener
+import com.personalization.sdk.data.models.params.UserBasicParams
+import com.rees46.rees46_flutter_sdk.pigeon.FlutterError
+import com.rees46.rees46_flutter_sdk.pigeon.PurchaseLineItemWire
+import org.json.JSONArray
+import org.json.JSONException
+import org.json.JSONObject
+
+/**
+ * Builds tracking JSON and posts via [SDK.sendAsync] for published `rees46-sdk` artifacts that do not
+ * expose `SDK.trackEvent` / `SDK.trackPurchase` (wire format aligned with personalization-sdk
+ * `TrackEventManagerImpl` and `PurchaseTrackingJsonBuilder`).
+ */
+internal object FlutterTrackingBridge {
+ private const val CUSTOM_PUSH_PATH = "push/custom"
+ private const val PURCHASE_PUSH_PATH = "push"
+
+ private const val TRACK_EVENT_CLIENT_ERROR_CODE = -1
+ private const val TRACK_PURCHASE_CLIENT_ERROR_CODE = -2
+
+ private const val KEY_EVENT = "event"
+ private const val KEY_TIME = "time"
+ private const val KEY_CATEGORY = "category"
+ private const val KEY_LABEL = "label"
+ private const val KEY_VALUE = "value"
+ private const val KEY_SOURCE = "source"
+ private const val KEY_PAYLOAD = "payload"
+ private const val KEY_FROM = "from"
+ private const val KEY_CODE = "code"
+ private const val KEY_STREAM = "stream"
+
+ private val RESERVED_CUSTOM_EVENT_KEYS: Set =
+ buildSet {
+ add(UserBasicParams.SHOP_ID)
+ add(UserBasicParams.DID)
+ add(UserBasicParams.SEANCE)
+ add(UserBasicParams.SID)
+ add(UserBasicParams.SEGMENT)
+ add(KEY_STREAM)
+ add(KEY_EVENT)
+ add(KEY_TIME)
+ add(KEY_CATEGORY)
+ add(KEY_LABEL)
+ add(KEY_VALUE)
+ add(KEY_SOURCE)
+ add(KEY_PAYLOAD)
+ add(KEY_FROM)
+ add(KEY_CODE)
+ }
+
+ private object PurchaseWireKeys {
+ const val EVENT = "event"
+ const val PURCHASE_EVENT_VALUE = "purchase"
+ const val ITEMS = "items"
+ const val ID = "id"
+ const val AMOUNT = "amount"
+ const val PRICE = "price"
+ const val LINE_ID = "line_id"
+ const val FASHION_SIZE = "fashion_size"
+ const val ORDER_ID = "order_id"
+ const val ORDER_PRICE = "order_price"
+ const val DELIVERY_TYPE = "delivery_type"
+ const val DELIVERY_ADDRESS = "delivery_address"
+ const val PAYMENT_TYPE = "payment_type"
+ const val TAX_FREE = "tax_free"
+ const val PROMOCODE = "promocode"
+ const val ORDER_CASH = "order_cash"
+ const val ORDER_BONUSES = "order_bonuses"
+ const val ORDER_DELIVERY = "order_delivery"
+ const val ORDER_DISCOUNT = "order_discount"
+ const val CHANNEL = "channel"
+ const val CUSTOM = "custom"
+ const val RECOMMENDED_SOURCE = "recommended_source"
+ const val RECOMMENDED_BY = "recommended_by"
+ const val RECOMMENDED_CODE = "recommended_code"
+ }
+
+ private val RESERVED_PURCHASE_CUSTOM_KEYS: Set =
+ RESERVED_CUSTOM_EVENT_KEYS +
+ setOf(
+ PurchaseWireKeys.EVENT,
+ PurchaseWireKeys.ITEMS,
+ PurchaseWireKeys.ORDER_ID,
+ PurchaseWireKeys.ORDER_PRICE,
+ PurchaseWireKeys.DELIVERY_TYPE,
+ PurchaseWireKeys.DELIVERY_ADDRESS,
+ PurchaseWireKeys.PAYMENT_TYPE,
+ PurchaseWireKeys.TAX_FREE,
+ PurchaseWireKeys.PROMOCODE,
+ PurchaseWireKeys.ORDER_CASH,
+ PurchaseWireKeys.ORDER_BONUSES,
+ PurchaseWireKeys.ORDER_DELIVERY,
+ PurchaseWireKeys.ORDER_DISCOUNT,
+ PurchaseWireKeys.CHANNEL,
+ PurchaseWireKeys.CUSTOM,
+ PurchaseWireKeys.RECOMMENDED_SOURCE,
+ PurchaseWireKeys.RECOMMENDED_BY,
+ PurchaseWireKeys.RECOMMENDED_CODE,
+ )
+
+ fun postTrackEvent(
+ event: String,
+ time: Long?,
+ category: String?,
+ label: String?,
+ value: Long?,
+ customFields: Map?,
+ callback: (Result) -> Unit,
+ ) {
+ val effectiveCustom = effectiveCustomFields(customFields)
+ validateNoReservedKeyCollisions(effectiveCustom, RESERVED_CUSTOM_EVENT_KEYS)?.let { msg ->
+ callback(
+ Result.failure(
+ FlutterError(
+ "track_event_failed",
+ msg,
+ mapOf("code" to TRACK_EVENT_CLIENT_ERROR_CODE),
+ ),
+ ),
+ )
+ return
+ }
+
+ val body = JSONObject()
+ try {
+ body.put(KEY_EVENT, event)
+ time?.let { body.put(KEY_TIME, longToJsonInt(it)) }
+ category?.let { body.put(KEY_CATEGORY, it) }
+ label?.let { body.put(KEY_LABEL, it) }
+ value?.let { body.put(KEY_VALUE, longToJsonInt(it)) }
+ if (effectiveCustom.isNotEmpty()) {
+ val payload = JSONObject()
+ for ((key, fieldValue) in effectiveCustom) {
+ putJsonValue(body, key, fieldValue)
+ putJsonValue(payload, key, fieldValue)
+ }
+ body.put(KEY_PAYLOAD, payload)
+ }
+ } catch (e: JSONException) {
+ callback(
+ Result.failure(
+ FlutterError(
+ "track_event_failed",
+ "trackEvent: failed to build JSON: ${e.message}",
+ null,
+ ),
+ ),
+ )
+ return
+ }
+
+ @Suppress("DEPRECATION")
+ SDK.instance.sendAsync(
+ CUSTOM_PUSH_PATH,
+ body,
+ object : OnApiCallbackListener() {
+ override fun onSuccess(response: JSONObject?) {
+ callback(Result.success(Unit))
+ }
+
+ override fun onError(code: Int, msg: String?) {
+ val message = listOfNotNull(code.toString(), msg).joinToString(": ")
+ callback(Result.failure(FlutterError("track_event_failed", message, null)))
+ }
+ },
+ )
+ }
+
+ fun postTrackPurchase(
+ orderId: String,
+ orderPrice: Double,
+ items: List,
+ deliveryType: String?,
+ deliveryAddress: String?,
+ paymentType: String?,
+ isTaxFree: Boolean,
+ promocode: String?,
+ orderCash: Double?,
+ orderBonuses: Double?,
+ orderDelivery: Double?,
+ orderDiscount: Double?,
+ channel: String?,
+ custom: Map?,
+ recommendedSource: JSONObject?,
+ stream: String?,
+ segment: String?,
+ callback: (Result) -> Unit,
+ ) {
+ val buildResult = buildPurchaseJsonOrError(
+ orderId = orderId,
+ orderPrice = orderPrice,
+ items = items,
+ deliveryType = deliveryType,
+ deliveryAddress = deliveryAddress,
+ paymentType = paymentType,
+ isTaxFree = isTaxFree,
+ promocode = promocode,
+ orderCash = orderCash,
+ orderBonuses = orderBonuses,
+ orderDelivery = orderDelivery,
+ orderDiscount = orderDiscount,
+ channel = channel,
+ custom = custom,
+ recommendedSource = recommendedSource,
+ stream = stream,
+ segment = segment,
+ )
+ if (buildResult.isFailure) {
+ callback(
+ Result.failure(
+ FlutterError(
+ "track_purchase_failed",
+ buildResult.exceptionOrNull()?.message ?: "validation failed",
+ mapOf("code" to TRACK_PURCHASE_CLIENT_ERROR_CODE),
+ ),
+ ),
+ )
+ return
+ }
+ val body = buildResult.getOrNull()!!
+
+ @Suppress("DEPRECATION")
+ SDK.instance.sendAsync(
+ PURCHASE_PUSH_PATH,
+ body,
+ object : OnApiCallbackListener() {
+ override fun onSuccess(response: JSONObject?) {
+ callback(Result.success(Unit))
+ }
+
+ override fun onError(code: Int, msg: String?) {
+ val message = listOfNotNull(code.toString(), msg).joinToString(": ")
+ callback(Result.failure(FlutterError("track_purchase_failed", message, null)))
+ }
+ },
+ )
+ }
+
+ private fun buildPurchaseJsonOrError(
+ orderId: String,
+ orderPrice: Double,
+ items: List,
+ deliveryType: String?,
+ deliveryAddress: String?,
+ paymentType: String?,
+ isTaxFree: Boolean,
+ promocode: String?,
+ orderCash: Double?,
+ orderBonuses: Double?,
+ orderDelivery: Double?,
+ orderDiscount: Double?,
+ channel: String?,
+ custom: Map?,
+ recommendedSource: JSONObject?,
+ stream: String?,
+ segment: String?,
+ ): Result {
+ if (orderId.isBlank()) {
+ return Result.failure(IllegalArgumentException("trackPurchase: orderId must be non-empty"))
+ }
+ if (items.isEmpty()) {
+ return Result.failure(IllegalArgumentException("trackPurchase: items must not be empty"))
+ }
+ for (item in items) {
+ if (item.id.isBlank()) {
+ return Result.failure(IllegalArgumentException("trackPurchase: each item.id must be non-empty"))
+ }
+ if (item.amount <= 0) {
+ return Result.failure(IllegalArgumentException("trackPurchase: each item.amount must be > 0"))
+ }
+ if (!item.price.isFinite()) {
+ return Result.failure(IllegalArgumentException("trackPurchase: each item.price must be a finite number"))
+ }
+ }
+ if (!orderPrice.isFinite()) {
+ return Result.failure(IllegalArgumentException("trackPurchase: orderPrice must be a finite number"))
+ }
+
+ val effectiveCustom = effectiveCustomFields(custom)
+ if (effectiveCustom.isNotEmpty()) {
+ val collisions = effectiveCustom.keys.intersect(RESERVED_PURCHASE_CUSTOM_KEYS)
+ if (collisions.isNotEmpty()) {
+ return Result.failure(
+ IllegalArgumentException(
+ "trackPurchase: custom contains reserved keys: ${collisions.toSortedSet().joinToString(", ")}",
+ ),
+ )
+ }
+ }
+
+ return try {
+ Result.success(
+ buildPurchaseJson(
+ orderId = orderId,
+ orderPrice = orderPrice,
+ items = items,
+ deliveryType = deliveryType,
+ deliveryAddress = deliveryAddress,
+ paymentType = paymentType,
+ isTaxFree = isTaxFree,
+ promocode = promocode,
+ orderCash = orderCash,
+ orderBonuses = orderBonuses,
+ orderDelivery = orderDelivery,
+ orderDiscount = orderDiscount,
+ channel = channel,
+ effectiveCustom = effectiveCustom,
+ recommendedSource = recommendedSource,
+ stream = stream,
+ segment = segment,
+ ),
+ )
+ } catch (e: JSONException) {
+ Result.failure(IllegalArgumentException("trackPurchase: failed to build JSON: ${e.message}", e))
+ }
+ }
+
+ private fun buildPurchaseJson(
+ orderId: String,
+ orderPrice: Double,
+ items: List,
+ deliveryType: String?,
+ deliveryAddress: String?,
+ paymentType: String?,
+ isTaxFree: Boolean,
+ promocode: String?,
+ orderCash: Double?,
+ orderBonuses: Double?,
+ orderDelivery: Double?,
+ orderDiscount: Double?,
+ channel: String?,
+ effectiveCustom: Map,
+ recommendedSource: JSONObject?,
+ stream: String?,
+ segment: String?,
+ ): JSONObject {
+ val root = JSONObject()
+ root.put(PurchaseWireKeys.EVENT, PurchaseWireKeys.PURCHASE_EVENT_VALUE)
+ root.put(PurchaseWireKeys.ORDER_ID, orderId)
+ root.put(PurchaseWireKeys.ORDER_PRICE, orderPrice)
+
+ val itemsArray = JSONArray()
+ for (item in items) {
+ val row = JSONObject()
+ row.put(PurchaseWireKeys.ID, item.id)
+ row.put(PurchaseWireKeys.AMOUNT, item.amount)
+ row.put(PurchaseWireKeys.PRICE, item.price)
+ item.lineId?.takeIf { it.isNotBlank() }?.let { row.put(PurchaseWireKeys.LINE_ID, it) }
+ item.fashionSize?.takeIf { it.isNotBlank() }?.let {
+ row.put(PurchaseWireKeys.FASHION_SIZE, it)
+ }
+ itemsArray.put(row)
+ }
+ root.put(PurchaseWireKeys.ITEMS, itemsArray)
+
+ deliveryType?.takeIf { it.isNotBlank() }?.let {
+ root.put(PurchaseWireKeys.DELIVERY_TYPE, it)
+ }
+ deliveryAddress?.takeIf { it.isNotBlank() }?.let {
+ root.put(PurchaseWireKeys.DELIVERY_ADDRESS, it)
+ }
+ paymentType?.takeIf { it.isNotBlank() }?.let {
+ root.put(PurchaseWireKeys.PAYMENT_TYPE, it)
+ }
+ if (isTaxFree) {
+ root.put(PurchaseWireKeys.TAX_FREE, true)
+ }
+ promocode?.takeIf { it.isNotBlank() }?.let {
+ root.put(PurchaseWireKeys.PROMOCODE, it)
+ }
+ orderCash?.let { root.put(PurchaseWireKeys.ORDER_CASH, it) }
+ orderBonuses?.let { root.put(PurchaseWireKeys.ORDER_BONUSES, it) }
+ orderDelivery?.let { root.put(PurchaseWireKeys.ORDER_DELIVERY, it) }
+ orderDiscount?.let { root.put(PurchaseWireKeys.ORDER_DISCOUNT, it) }
+ channel?.takeIf { it.isNotBlank() }?.let {
+ root.put(PurchaseWireKeys.CHANNEL, it)
+ }
+
+ if (effectiveCustom.isNotEmpty()) {
+ val customJson = JSONObject()
+ for ((key, value) in effectiveCustom) {
+ putJsonValue(customJson, key, value)
+ }
+ root.put(PurchaseWireKeys.CUSTOM, customJson)
+ }
+
+ recommendedSource?.let { root.put(PurchaseWireKeys.RECOMMENDED_SOURCE, it) }
+
+ stream?.takeIf { it.isNotBlank() }?.let {
+ root.put(UserBasicParams.STREAM, it)
+ }
+ segment?.takeIf { it.isNotBlank() }?.let {
+ root.put(UserBasicParams.SEGMENT, it)
+ }
+
+ return root
+ }
+
+ private fun effectiveCustomFields(map: Map?): Map {
+ if (map.isNullOrEmpty()) return emptyMap()
+ val out = LinkedHashMap()
+ for ((key, value) in map) {
+ if (key.isBlank() || value == null) continue
+ out[key] = value
+ }
+ return out
+ }
+
+ private fun validateNoReservedKeyCollisions(
+ customFields: Map,
+ reserved: Set,
+ ): String? {
+ if (customFields.isEmpty()) return null
+ val collisions = customFields.keys.intersect(reserved)
+ if (collisions.isEmpty()) return null
+ val sorted = collisions.toSortedSet().joinToString(", ")
+ return "trackEvent: customFields contains reserved keys: $sorted"
+ }
+
+ @Throws(JSONException::class)
+ private fun putJsonValue(target: JSONObject, key: String, value: Any) {
+ when (value) {
+ is String -> target.put(key, value)
+ is Int -> target.put(key, value)
+ is Long -> target.put(key, value)
+ is Double -> target.put(key, value)
+ is Float -> target.put(key, value.toDouble())
+ is Boolean -> target.put(key, value)
+ is JSONObject -> target.put(key, value)
+ is JSONArray -> target.put(key, value)
+ else -> target.put(key, value.toString())
+ }
+ }
+
+ private fun longToJsonInt(value: Long): Int =
+ when {
+ value > Int.MAX_VALUE -> Int.MAX_VALUE
+ value < Int.MIN_VALUE -> Int.MIN_VALUE
+ else -> value.toInt()
+ }
+}
diff --git a/android/src/main/kotlin/com/rees46/rees46_flutter_sdk/Rees46FlutterSdkPlugin.kt b/android/src/main/kotlin/com/rees46/rees46_flutter_sdk/Rees46FlutterSdkPlugin.kt
new file mode 100644
index 0000000..9a06bfd
--- /dev/null
+++ b/android/src/main/kotlin/com/rees46/rees46_flutter_sdk/Rees46FlutterSdkPlugin.kt
@@ -0,0 +1,631 @@
+package com.rees46.rees46_flutter_sdk
+
+import android.content.Context
+import android.content.Intent
+import android.content.SharedPreferences
+import android.os.Bundle
+import android.os.SystemClock
+import com.rees46.rees46_flutter_sdk.pigeon.FlutterError
+import com.rees46.rees46_flutter_sdk.pigeon.InitConfig
+import com.rees46.rees46_flutter_sdk.pigeon.PersonalizationFlutterApi
+import com.rees46.rees46_flutter_sdk.pigeon.PersonalizationHostApi
+import com.rees46.rees46_flutter_sdk.pigeon.ProfileParamsWire
+import com.rees46.rees46_flutter_sdk.pigeon.PurchaseLineItemWire
+import com.google.gson.Gson
+import com.personalization.Params
+import com.personalization.SDK
+import com.personalization.api.OnApiCallbackListener
+import com.personalization.api.params.ProfileParams
+import com.personalization.api.params.SearchParams as NativeSearchParams
+import com.personalization.features.notification.presentation.helpers.NotificationImageHelper
+import com.personalization.sdk.data.models.dto.notification.NotificationData
+import io.flutter.embedding.engine.plugins.FlutterPlugin
+import io.flutter.embedding.engine.plugins.activity.ActivityAware
+import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
+import io.flutter.plugin.common.PluginRegistry
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import org.json.JSONArray
+import org.json.JSONObject
+
+/** Rees46FlutterSdkPlugin */
+class Rees46FlutterSdkPlugin :
+ FlutterPlugin,
+ ActivityAware,
+ PersonalizationHostApi {
+ private lateinit var applicationContext: Context
+ private val coroutineScope = CoroutineScope(Dispatchers.Main + Job())
+ private var flutterApi: PersonalizationFlutterApi? = null
+ private var activityBinding: ActivityPluginBinding? = null
+
+ private val onNewIntentListener =
+ object : PluginRegistry.NewIntentListener {
+ override fun onNewIntent(intent: Intent): Boolean {
+ this@Rees46FlutterSdkPlugin.handleNotificationLaunchIntent(intent)
+ return false
+ }
+ }
+
+ override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
+ applicationContext = flutterPluginBinding.applicationContext
+ PersonalizationHostApi.setUp(flutterPluginBinding.binaryMessenger, this)
+ flutterApi = PersonalizationFlutterApi(flutterPluginBinding.binaryMessenger)
+ }
+
+ override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
+ PersonalizationHostApi.setUp(binding.binaryMessenger, null)
+ flutterApi = null
+ coroutineScope.cancel()
+ }
+
+ override fun onAttachedToActivity(binding: ActivityPluginBinding) {
+ bindActivity(binding)
+ }
+
+ override fun onDetachedFromActivityForConfigChanges() {
+ unbindActivity()
+ }
+
+ override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
+ bindActivity(binding)
+ }
+
+ override fun onDetachedFromActivity() {
+ unbindActivity()
+ }
+
+ private fun bindActivity(binding: ActivityPluginBinding) {
+ unbindActivity()
+ activityBinding = binding
+ binding.addOnNewIntentListener(onNewIntentListener)
+ handleNotificationLaunchIntent(binding.activity.intent)
+ }
+
+ private fun unbindActivity() {
+ activityBinding?.removeOnNewIntentListener(onNewIntentListener)
+ activityBinding = null
+ }
+
+ override fun getPlatformVersion(): String = "Android ${android.os.Build.VERSION.RELEASE}"
+
+ override fun getStoredPushToken(): String? {
+ val prefs: SharedPreferences =
+ applicationContext.getSharedPreferences(DEFAULT_STORAGE_KEY, Context.MODE_PRIVATE)
+ return prefs.getString(TOKEN_KEY, null)
+ ?.takeIf { it.isNotBlank() }
+ }
+
+ override fun initialize(config: InitConfig, callback: (Result) -> Unit) {
+ val shopId = config.shopId
+ if (shopId.isBlank()) {
+ callback(Result.failure(FlutterError("bad_args", "shopId is required", null)))
+ return
+ }
+ try {
+ val sdk = SDK.instance
+ sdk.initialize(
+ context = applicationContext,
+ shopId = shopId,
+ apiDomain = config.apiDomain,
+ stream = config.stream,
+ autoSendPushToken = config.autoSendPushToken,
+ needReInitialization = config.needReInitialization,
+ )
+
+ // Mirror REES46 entrypoint behaviour: show notifications on message.
+ sdk.setOnMessageListener { data ->
+ val payload = data.toPayload()
+ flutterApi?.onPushReceived(payload) { _ -> }
+ coroutineScope.launch {
+ val (images, hasError) = withContext(Dispatchers.IO) {
+ NotificationImageHelper.loadBitmaps(urls = data.image)
+ }
+ sdk.notificationHelper.createNotification(
+ context = applicationContext,
+ data = NotificationData(
+ id = data.id,
+ title = data.title,
+ body = data.body,
+ icon = data.icon,
+ type = data.type,
+ actions = data.actions,
+ actionUrls = data.actionUrls,
+ image = data.image,
+ event = data.event,
+ ),
+ images = images,
+ hasError = hasError,
+ )
+ flutterApi?.onPushDelivered(payload) { _ -> }
+ }
+ }
+
+ callback(Result.success(Unit))
+ } catch (t: Throwable) {
+ callback(Result.failure(FlutterError("init_failed", t.message, null)))
+ }
+ }
+
+ override fun getRecommendation(
+ code: String,
+ paramsJson: String?,
+ callback: (Result) -> Unit,
+ ) {
+ if (code.isBlank()) {
+ callback(Result.failure(FlutterError("bad_args", "code is required", null)))
+ return
+ }
+ try {
+ val params = buildRecommendationParams(paramsJson)
+ SDK.instance.recommendationManager.getExtendedRecommendation(
+ recommenderCode = code,
+ params = params,
+ onGetExtendedRecommendation = { response ->
+ callback(Result.success(Gson().toJson(response)))
+ },
+ onError = { code, message ->
+ callback(Result.failure(FlutterError("recommendation_failed", message ?: "error $code", null)))
+ },
+ )
+ } catch (t: Throwable) {
+ callback(Result.failure(FlutterError("recommendation_failed", t.message, null)))
+ }
+ }
+
+ override fun getProductInfo(itemId: String, callback: (Result) -> Unit) {
+ if (itemId.isBlank()) {
+ callback(Result.failure(FlutterError("bad_args", "itemId is required", null)))
+ return
+ }
+ try {
+ SDK.instance.productsManager.getProductInfo(
+ itemId = itemId,
+ listener = object : OnApiCallbackListener() {
+ override fun onSuccess(response: org.json.JSONObject?) {
+ if (response != null) {
+ callback(Result.success(response.toString()))
+ } else {
+ callback(Result.failure(FlutterError("product_info_failed", "Empty response", null)))
+ }
+ }
+ }
+ )
+ } catch (t: Throwable) {
+ callback(Result.failure(FlutterError("product_info_failed", t.message, null)))
+ }
+ }
+
+ override fun getProductsList(paramsJson: String?, callback: (Result) -> Unit) {
+ try {
+ val p = if (!paramsJson.isNullOrBlank()) JSONObject(paramsJson) else null
+ val brands = p?.optString("brands")?.takeIf { it.isNotEmpty() }
+ val merchants = p?.optString("merchants")?.takeIf { it.isNotEmpty() }
+ val categories = p?.optString("categories")?.takeIf { it.isNotEmpty() }
+ val locations = p?.optString("locations")?.takeIf { it.isNotEmpty() }
+ val limit = if (p?.has("limit") == true) p.optInt("limit") else null
+ val page = if (p?.has("page") == true) p.optInt("page") else null
+ val filters: Map? = p?.optJSONObject("filters")?.let { obj ->
+ obj.keys().asSequence().associateWith { key -> obj.get(key) }
+ }
+ SDK.instance.productsManager.getProductsList(
+ brands = brands,
+ merchants = merchants,
+ categories = categories,
+ locations = locations,
+ limit = limit,
+ page = page,
+ filters = filters,
+ listener = object : OnApiCallbackListener() {
+ override fun onSuccess(response: org.json.JSONObject?) {
+ if (response != null) {
+ callback(Result.success(response.toString()))
+ } else {
+ callback(Result.failure(FlutterError("products_list_failed", "Empty response", null)))
+ }
+ }
+ }
+ )
+ } catch (t: Throwable) {
+ callback(Result.failure(FlutterError("products_list_failed", t.message, null)))
+ }
+ }
+
+ override fun searchBlank(callback: (Result) -> Unit) {
+ try {
+ SDK.instance.searchManager.searchBlank(
+ onSearchBlank = { response ->
+ callback(Result.success(Gson().toJson(response)))
+ },
+ onError = { code, message ->
+ callback(Result.failure(FlutterError("search_blank_failed", message ?: "error $code", null)))
+ },
+ )
+ } catch (t: Throwable) {
+ callback(Result.failure(FlutterError("search_blank_failed", t.message, null)))
+ }
+ }
+
+ override fun searchInstant(
+ query: String,
+ paramsJson: String?,
+ callback: (Result) -> Unit,
+ ) {
+ if (query.isBlank()) {
+ callback(Result.failure(FlutterError("bad_args", "query is required", null)))
+ return
+ }
+ try {
+ val json = if (!paramsJson.isNullOrBlank()) JSONObject(paramsJson) else null
+ val locations = json?.optString("locations")?.takeIf { it.isNotEmpty() }
+ val excludedBrands = jsonArrayToStringList(json?.optJSONArray("excluded_brands"))
+ SDK.instance.searchManager.searchInstant(
+ query = query,
+ locations = locations,
+ excludedMerchants = null,
+ excludedBrands = excludedBrands,
+ onSearchInstant = { response ->
+ callback(Result.success(Gson().toJson(response)))
+ },
+ onError = { code, message ->
+ callback(Result.failure(FlutterError("search_instant_failed", message ?: "error $code", null)))
+ },
+ )
+ } catch (t: Throwable) {
+ callback(Result.failure(FlutterError("search_instant_failed", t.message, null)))
+ }
+ }
+
+ override fun searchFull(
+ query: String,
+ paramsJson: String?,
+ callback: (Result) -> Unit,
+ ) {
+ if (query.isBlank()) {
+ callback(Result.failure(FlutterError("bad_args", "query is required", null)))
+ return
+ }
+ try {
+ val params = buildSearchParams(paramsJson)
+ SDK.instance.searchManager.searchFull(
+ query = query,
+ searchParams = params,
+ onSearchFull = { response ->
+ callback(Result.success(Gson().toJson(response)))
+ },
+ onError = { code, message ->
+ callback(Result.failure(FlutterError("search_failed", message ?: "error $code", null)))
+ },
+ )
+ } catch (t: Throwable) {
+ callback(Result.failure(FlutterError("search_failed", t.message, null)))
+ }
+ }
+
+ override fun getSid(): String = SDK.instance.getSid()
+
+ override fun getDid(): String? = SDK.instance.getDid()
+
+ override fun setProfile(params: ProfileParamsWire, callback: (Result) -> Unit) {
+ try {
+ val builder = ProfileParams.Builder()
+ params.email?.let { builder.put("email", it) }
+ params.phone?.let { builder.put("phone", it) }
+ params.loyaltyId?.let { builder.put("loyalty_id", it) }
+ params.firstName?.let { builder.put("first_name", it) }
+ params.lastName?.let { builder.put("last_name", it) }
+ params.birthday?.let { builder.put("birthday", it) }
+ params.age?.let { builder.put("age", it.toInt()) }
+ params.gender?.let { builder.put("gender", it) }
+ params.location?.let { builder.put("location", it) }
+ params.advertisingId?.let { builder.put("advertising_id", it) }
+ params.fbId?.let { builder.put("fb_id", it) }
+ params.vkId?.let { builder.put("vk_id", it) }
+ params.telegramId?.let { builder.put("telegram_id", it) }
+ params.loyaltyCardLocation?.let { builder.put("loyalty_card_location", it) }
+ params.loyaltyStatus?.let { builder.put("loyalty_status", it) }
+ params.loyaltyBonuses?.let { builder.put("loyalty_bonuses", it.toInt()) }
+ params.loyaltyBonusesToNextLevel?.let { builder.put("loyalty_bonuses_to_next_level", it.toInt()) }
+ params.boughtSomething?.let { builder.put("bought_something", if (it) "1" else "0") }
+ params.userId?.let { builder.put("id", it) }
+ params.customPropertiesJson?.let { json ->
+ val obj = JSONObject(json)
+ obj.keys().forEach { key -> builder.put(key, obj.getString(key)) }
+ }
+ SDK.instance.profile(builder.build(), object : OnApiCallbackListener() {
+ override fun onSuccess(response: JSONObject?) {
+ callback(Result.success(Unit))
+ }
+ override fun onError(code: Int, msg: String?) {
+ callback(Result.failure(FlutterError("set_profile_failed", msg ?: "error $code", null)))
+ }
+ })
+ } catch (t: Throwable) {
+ callback(Result.failure(FlutterError("set_profile_failed", t.message, null)))
+ }
+ }
+
+ override fun trackEvent(
+ event: String,
+ time: Long?,
+ category: String?,
+ label: String?,
+ value: Long?,
+ customFieldsJson: String?,
+ callback: (Result) -> Unit,
+ ) {
+ if (event.isBlank()) {
+ callback(Result.failure(FlutterError("bad_args", "event is required", null)))
+ return
+ }
+ try {
+ val customFields = jsonObjectStringToMap(customFieldsJson)
+ FlutterTrackingBridge.postTrackEvent(
+ event = event,
+ time = time,
+ category = category,
+ label = label,
+ value = value,
+ customFields = customFields,
+ callback = callback,
+ )
+ } catch (t: Throwable) {
+ callback(Result.failure(FlutterError("track_event_failed", t.message, null)))
+ }
+ }
+
+ override fun trackPurchase(
+ orderId: String,
+ orderPrice: Double,
+ items: List,
+ deliveryType: String?,
+ deliveryAddress: String?,
+ paymentType: String?,
+ isTaxFree: Boolean,
+ promocode: String?,
+ orderCash: Double?,
+ orderBonuses: Double?,
+ orderDelivery: Double?,
+ orderDiscount: Double?,
+ channel: String?,
+ customJson: String?,
+ recommendedSourceJson: String?,
+ stream: String?,
+ segment: String?,
+ callback: (Result) -> Unit,
+ ) {
+ if (orderId.isBlank()) {
+ callback(Result.failure(FlutterError("bad_args", "orderId is required", null)))
+ return
+ }
+ if (items.isEmpty()) {
+ callback(Result.failure(FlutterError("bad_args", "items must be non-empty", null)))
+ return
+ }
+ try {
+ val recommendedSource =
+ if (recommendedSourceJson.isNullOrBlank()) {
+ null
+ } else {
+ JSONObject(recommendedSourceJson)
+ }
+ FlutterTrackingBridge.postTrackPurchase(
+ orderId = orderId,
+ orderPrice = orderPrice,
+ items = items,
+ deliveryType = deliveryType,
+ deliveryAddress = deliveryAddress,
+ paymentType = paymentType,
+ isTaxFree = isTaxFree,
+ promocode = promocode,
+ orderCash = orderCash,
+ orderBonuses = orderBonuses,
+ orderDelivery = orderDelivery,
+ orderDiscount = orderDiscount,
+ channel = channel,
+ custom = jsonObjectStringToMap(customJson),
+ recommendedSource = recommendedSource,
+ stream = stream,
+ segment = segment,
+ callback = callback,
+ )
+ } catch (t: Throwable) {
+ callback(Result.failure(FlutterError("track_purchase_failed", t.message, null)))
+ }
+ }
+
+ private fun handleNotificationLaunchIntent(intent: Intent?) {
+ val extras = intent?.extras ?: return
+ if (!extras.isPersonalizationNotificationClick()) {
+ return
+ }
+ val payload = extras.toStringPayloadMap()
+ val type = payload[NotificationClickExtraKeys.NOTIFICATION_TYPE] ?: return
+ val id = payload[NotificationClickExtraKeys.NOTIFICATION_ID] ?: return
+ val signature = "$type|$id"
+ if (!shouldProcessClickSignature(signature)) {
+ return
+ }
+ try {
+ SDK.instance.notificationClicked(extras)
+ flutterApi?.onPushClicked(payload) { _ -> }
+ } catch (_: Throwable) {
+ // SDK may not be initialized yet; ignore.
+ }
+ }
+
+ companion object {
+ private const val NOTIFICATION_CLICK_DEBOUNCE_MS = 800L
+ private const val DEFAULT_STORAGE_KEY = "DEFAULT_STORAGE_KEY"
+ private const val TOKEN_KEY = "token"
+
+ private var lastClickSignature: String? = null
+ private var lastClickAtElapsedMs: Long = 0L
+
+ private fun shouldProcessClickSignature(signature: String): Boolean {
+ val now = SystemClock.elapsedRealtime()
+ if (signature == lastClickSignature && now - lastClickAtElapsedMs < NOTIFICATION_CLICK_DEBOUNCE_MS) {
+ return false
+ }
+ lastClickSignature = signature
+ lastClickAtElapsedMs = now
+ return true
+ }
+ }
+
+}
+
+private fun buildSearchParams(paramsJson: String?): NativeSearchParams {
+ val params = NativeSearchParams()
+ if (paramsJson.isNullOrBlank()) return params
+ val json = JSONObject(paramsJson)
+ json.optInt("limit").takeIf { it > 0 }
+ ?.let { params.put(NativeSearchParams.Parameter.LIMIT, it) }
+ json.optInt("page").takeIf { it > 0 }
+ ?.let { params.put(NativeSearchParams.Parameter.PAGE, it) }
+ json.optInt("category_limit").takeIf { it > 0 }
+ ?.let { params.put(NativeSearchParams.Parameter.CATEGORY_LIMIT, it) }
+ json.optInt("brand_limit").takeIf { it > 0 }
+ ?.let { params.put(NativeSearchParams.Parameter.BRAND_LIMIT, it) }
+ json.optString("sort_by").takeIf { it.isNotEmpty() }
+ ?.let { params.put(NativeSearchParams.Parameter.SORT_BY, it) }
+ json.optString("sort_dir").takeIf { it.isNotEmpty() }
+ ?.let { params.put(NativeSearchParams.Parameter.SORT_DIR, it) }
+ json.optString("locations").takeIf { it.isNotEmpty() }
+ ?.let { params.put(NativeSearchParams.Parameter.LOCATIONS, it) }
+ json.optString("brands").takeIf { it.isNotEmpty() }
+ ?.let { params.put(NativeSearchParams.Parameter.BRANDS, it) }
+ if (json.has("price_min"))
+ params.put(NativeSearchParams.Parameter.PRICE_MIN, json.getDouble("price_min").toString())
+ if (json.has("price_max"))
+ params.put(NativeSearchParams.Parameter.PRICE_MAX, json.getDouble("price_max").toString())
+ jsonArrayToStringArray(json.optJSONArray("categories"))
+ ?.let { params.put(NativeSearchParams.Parameter.CATEGORIES, it) }
+ jsonArrayToStringArray(json.optJSONArray("excluded_brands"))
+ ?.let { params.put(NativeSearchParams.Parameter.EXCLUDED_BRANDS, it) }
+ jsonArrayToStringArray(json.optJSONArray("colors"))
+ ?.let { params.put(NativeSearchParams.Parameter.COLORS, it) }
+ jsonArrayToStringArray(json.optJSONArray("fashion_sizes"))
+ ?.let { params.put(NativeSearchParams.Parameter.FASHION_SIZES, it) }
+ return params
+}
+
+private fun jsonArrayToStringArray(arr: org.json.JSONArray?): Array? {
+ if (arr == null || arr.length() == 0) return null
+ return Array(arr.length()) { i -> arr.getString(i) }
+}
+
+private fun jsonArrayToStringList(arr: org.json.JSONArray?): List? {
+ if (arr == null || arr.length() == 0) return null
+ return (0 until arr.length()).map { arr.getString(it) }
+}
+
+private fun buildRecommendationParams(paramsJson: String?): Params {
+ val params = Params()
+ if (paramsJson.isNullOrBlank()) return params
+ val json = JSONObject(paramsJson)
+ json.optString("item_id").takeIf { it.isNotEmpty() }
+ ?.let { params.put(Params.Parameter.ITEM, it) }
+ json.optString("category_id").takeIf { it.isNotEmpty() }
+ ?.let { params.put(Params.Parameter.CATEGORY_ID, it) }
+ json.optString("locations").takeIf { it.isNotEmpty() }
+ ?.let { params.put(Params.Parameter.LOCATIONS, it) }
+ if (json.has("image_size"))
+ params.put(Params.Parameter.IMAGE_SIZE, json.getInt("image_size").toString())
+ if (json.has("with_locations"))
+ params.put(Params.Parameter.WITH_LOCATIONS, json.getBoolean("with_locations").toString())
+ return params
+}
+
+private fun jsonObjectStringToMap(json: String?): Map? {
+ if (json.isNullOrBlank()) return null
+ val root = JSONObject(json)
+ return jsonObjectToNestedMap(root)
+}
+
+private fun jsonObjectToNestedMap(obj: JSONObject): Map {
+ val out = LinkedHashMap()
+ val keys = obj.keys()
+ while (keys.hasNext()) {
+ val key = keys.next()
+ val raw = obj.get(key)
+ out[key] = jsonValueToKotlin(raw)
+ }
+ return out
+}
+
+private fun jsonValueToKotlin(value: Any?): Any? {
+ return when (value) {
+ null, JSONObject.NULL -> null
+ is JSONObject -> jsonObjectToNestedMap(value)
+ is JSONArray -> jsonArrayToList(value)
+ else -> value
+ }
+}
+
+private fun jsonArrayToList(arr: JSONArray): List {
+ val list = ArrayList(arr.length())
+ for (i in 0 until arr.length()) {
+ list.add(jsonValueToKotlin(arr.opt(i)))
+ }
+ return list
+}
+
+/** Matches [com.personalization.features.notification.domain.model.NotificationConstants]. */
+private object NotificationClickExtraKeys {
+ const val NOTIFICATION_TYPE = "NOTIFICATION_TYPE"
+ const val NOTIFICATION_ID = "NOTIFICATION_ID"
+}
+
+private fun Bundle.isPersonalizationNotificationClick(): Boolean {
+ return !getString(NotificationClickExtraKeys.NOTIFICATION_TYPE).isNullOrBlank() &&
+ !getString(NotificationClickExtraKeys.NOTIFICATION_ID).isNullOrBlank()
+}
+
+private fun Bundle.toStringPayloadMap(): Map {
+ val map = mutableMapOf()
+ for (key in keySet()) {
+ val value = get(key) ?: continue
+ map[key] = value.toString()
+ }
+ return map
+}
+
+private fun NotificationData.toPayload(): Map {
+ val map = mutableMapOf()
+ map["id"] = id
+ map["title"] = title
+ map["body"] = body
+ map["icon"] = icon
+ map["type"] = type
+ map["image"] = image
+ if (!actions.isNullOrEmpty()) {
+ val arr = JSONArray()
+ actions!!.forEach { action ->
+ arr.put(
+ JSONObject().put("action", action.action).put("title", action.title)
+ )
+ }
+ map["actions"] = arr.toString()
+ }
+ if (!actionUrls.isNullOrEmpty()) {
+ map["actionUrls"] = actionUrls!!.joinToString(",")
+ }
+ event?.let { ev ->
+ val json = JSONObject()
+ json.put("type", ev.type ?: JSONObject.NULL)
+ json.put("uri", ev.uri ?: JSONObject.NULL)
+ ev.payload?.let { payload ->
+ try {
+ json.put("payload", JSONObject(payload))
+ } catch (_: Exception) {
+ json.put("payload", payload.toString())
+ }
+ }
+ map["event"] = json.toString()
+ }
+ return map
+}
diff --git a/android/src/main/kotlin/com/rees46/rees46_flutter_sdk/pigeon/PersonalizationApi.g.kt b/android/src/main/kotlin/com/rees46/rees46_flutter_sdk/pigeon/PersonalizationApi.g.kt
new file mode 100644
index 0000000..e4e16b8
--- /dev/null
+++ b/android/src/main/kotlin/com/rees46/rees46_flutter_sdk/pigeon/PersonalizationApi.g.kt
@@ -0,0 +1,710 @@
+// Autogenerated from Pigeon (v25.5.0), do not edit directly.
+// See also: https://pub.dev/packages/pigeon
+@file:Suppress("UNCHECKED_CAST", "ArrayInDataClass")
+
+package com.rees46.rees46_flutter_sdk.pigeon
+
+import android.util.Log
+import io.flutter.plugin.common.BasicMessageChannel
+import io.flutter.plugin.common.BinaryMessenger
+import io.flutter.plugin.common.EventChannel
+import io.flutter.plugin.common.MessageCodec
+import io.flutter.plugin.common.StandardMethodCodec
+import io.flutter.plugin.common.StandardMessageCodec
+import java.io.ByteArrayOutputStream
+import java.nio.ByteBuffer
+private object PersonalizationApiPigeonUtils {
+
+ fun createConnectionError(channelName: String): FlutterError {
+ return FlutterError("channel-error", "Unable to establish connection on channel: '$channelName'.", "") }
+
+ fun wrapResult(result: Any?): List {
+ return listOf(result)
+ }
+
+ fun wrapError(exception: Throwable): List {
+ return if (exception is FlutterError) {
+ listOf(
+ exception.code,
+ exception.message,
+ exception.details
+ )
+ } else {
+ listOf(
+ exception.javaClass.simpleName,
+ exception.toString(),
+ "Cause: " + exception.cause + ", Stacktrace: " + Log.getStackTraceString(exception)
+ )
+ }
+ }
+ fun deepEquals(a: Any?, b: Any?): Boolean {
+ if (a is ByteArray && b is ByteArray) {
+ return a.contentEquals(b)
+ }
+ if (a is IntArray && b is IntArray) {
+ return a.contentEquals(b)
+ }
+ if (a is LongArray && b is LongArray) {
+ return a.contentEquals(b)
+ }
+ if (a is DoubleArray && b is DoubleArray) {
+ return a.contentEquals(b)
+ }
+ if (a is Array<*> && b is Array<*>) {
+ return a.size == b.size &&
+ a.indices.all{ deepEquals(a[it], b[it]) }
+ }
+ if (a is List<*> && b is List<*>) {
+ return a.size == b.size &&
+ a.indices.all{ deepEquals(a[it], b[it]) }
+ }
+ if (a is Map<*, *> && b is Map<*, *>) {
+ return a.size == b.size && a.all {
+ (b as Map).containsKey(it.key) &&
+ deepEquals(it.value, b[it.key])
+ }
+ }
+ return a == b
+ }
+
+}
+
+/**
+ * Error class for passing custom error details to Flutter via a thrown PlatformException.
+ * @property code The error code.
+ * @property message The error message.
+ * @property details The error details. Must be a datatype supported by the api codec.
+ */
+class FlutterError (
+ val code: String,
+ override val message: String? = null,
+ val details: Any? = null
+) : Throwable()
+
+/** Generated class from Pigeon that represents data sent in messages. */
+data class InitConfig (
+ val shopId: String,
+ val apiDomain: String,
+ val stream: String,
+ val enableLogs: Boolean,
+ val autoSendPushToken: Boolean,
+ val sendAdvertisingId: Boolean,
+ val enableAutoPopupPresentation: Boolean,
+ val needReInitialization: Boolean
+)
+ {
+ companion object {
+ fun fromList(pigeonVar_list: List): InitConfig {
+ val shopId = pigeonVar_list[0] as String
+ val apiDomain = pigeonVar_list[1] as String
+ val stream = pigeonVar_list[2] as String
+ val enableLogs = pigeonVar_list[3] as Boolean
+ val autoSendPushToken = pigeonVar_list[4] as Boolean
+ val sendAdvertisingId = pigeonVar_list[5] as Boolean
+ val enableAutoPopupPresentation = pigeonVar_list[6] as Boolean
+ val needReInitialization = pigeonVar_list[7] as Boolean
+ return InitConfig(shopId, apiDomain, stream, enableLogs, autoSendPushToken, sendAdvertisingId, enableAutoPopupPresentation, needReInitialization)
+ }
+ }
+ fun toList(): List {
+ return listOf(
+ shopId,
+ apiDomain,
+ stream,
+ enableLogs,
+ autoSendPushToken,
+ sendAdvertisingId,
+ enableAutoPopupPresentation,
+ needReInitialization,
+ )
+ }
+ override fun equals(other: Any?): Boolean {
+ if (other !is InitConfig) {
+ return false
+ }
+ if (this === other) {
+ return true
+ }
+ return PersonalizationApiPigeonUtils.deepEquals(toList(), other.toList()) }
+
+ override fun hashCode(): Int = toList().hashCode()
+}
+
+/**
+ * Wire format for one purchase line (maps to native [PurchaseItemRequest]; quantity not exposed).
+ *
+ * Generated class from Pigeon that represents data sent in messages.
+ */
+data class PurchaseLineItemWire (
+ val id: String,
+ val amount: Long,
+ val price: Double,
+ val lineId: String? = null,
+ val fashionSize: String? = null
+)
+ {
+ companion object {
+ fun fromList(pigeonVar_list: List): PurchaseLineItemWire {
+ val id = pigeonVar_list[0] as String
+ val amount = pigeonVar_list[1] as Long
+ val price = pigeonVar_list[2] as Double
+ val lineId = pigeonVar_list[3] as String?
+ val fashionSize = pigeonVar_list[4] as String?
+ return PurchaseLineItemWire(id, amount, price, lineId, fashionSize)
+ }
+ }
+ fun toList(): List {
+ return listOf(
+ id,
+ amount,
+ price,
+ lineId,
+ fashionSize,
+ )
+ }
+ override fun equals(other: Any?): Boolean {
+ if (other !is PurchaseLineItemWire) {
+ return false
+ }
+ if (this === other) {
+ return true
+ }
+ return PersonalizationApiPigeonUtils.deepEquals(toList(), other.toList()) }
+
+ override fun hashCode(): Int = toList().hashCode()
+}
+
+/**
+ * Wire format for profile fields sent to native SDK.
+ * All fields are optional — only non-null values are forwarded.
+ * [birthday] must be a "yyyy-MM-dd" string.
+ * [gender] must be "m" or "f".
+ * [customPropertiesJson] is a JSON object string or null.
+ *
+ * Generated class from Pigeon that represents data sent in messages.
+ */
+data class ProfileParamsWire (
+ val email: String? = null,
+ val phone: String? = null,
+ val loyaltyId: String? = null,
+ val firstName: String? = null,
+ val lastName: String? = null,
+ val birthday: String? = null,
+ val age: Long? = null,
+ val gender: String? = null,
+ val location: String? = null,
+ val advertisingId: String? = null,
+ val fbId: String? = null,
+ val vkId: String? = null,
+ val telegramId: String? = null,
+ val loyaltyCardLocation: String? = null,
+ val loyaltyStatus: String? = null,
+ val loyaltyBonuses: Long? = null,
+ val loyaltyBonusesToNextLevel: Long? = null,
+ val boughtSomething: Boolean? = null,
+ val userId: String? = null,
+ val customPropertiesJson: String? = null
+)
+ {
+ companion object {
+ fun fromList(pigeonVar_list: List): ProfileParamsWire {
+ val email = pigeonVar_list[0] as String?
+ val phone = pigeonVar_list[1] as String?
+ val loyaltyId = pigeonVar_list[2] as String?
+ val firstName = pigeonVar_list[3] as String?
+ val lastName = pigeonVar_list[4] as String?
+ val birthday = pigeonVar_list[5] as String?
+ val age = pigeonVar_list[6] as Long?
+ val gender = pigeonVar_list[7] as String?
+ val location = pigeonVar_list[8] as String?
+ val advertisingId = pigeonVar_list[9] as String?
+ val fbId = pigeonVar_list[10] as String?
+ val vkId = pigeonVar_list[11] as String?
+ val telegramId = pigeonVar_list[12] as String?
+ val loyaltyCardLocation = pigeonVar_list[13] as String?
+ val loyaltyStatus = pigeonVar_list[14] as String?
+ val loyaltyBonuses = pigeonVar_list[15] as Long?
+ val loyaltyBonusesToNextLevel = pigeonVar_list[16] as Long?
+ val boughtSomething = pigeonVar_list[17] as Boolean?
+ val userId = pigeonVar_list[18] as String?
+ val customPropertiesJson = pigeonVar_list[19] as String?
+ return ProfileParamsWire(email, phone, loyaltyId, firstName, lastName, birthday, age, gender, location, advertisingId, fbId, vkId, telegramId, loyaltyCardLocation, loyaltyStatus, loyaltyBonuses, loyaltyBonusesToNextLevel, boughtSomething, userId, customPropertiesJson)
+ }
+ }
+ fun toList(): List {
+ return listOf(
+ email,
+ phone,
+ loyaltyId,
+ firstName,
+ lastName,
+ birthday,
+ age,
+ gender,
+ location,
+ advertisingId,
+ fbId,
+ vkId,
+ telegramId,
+ loyaltyCardLocation,
+ loyaltyStatus,
+ loyaltyBonuses,
+ loyaltyBonusesToNextLevel,
+ boughtSomething,
+ userId,
+ customPropertiesJson,
+ )
+ }
+ override fun equals(other: Any?): Boolean {
+ if (other !is ProfileParamsWire) {
+ return false
+ }
+ if (this === other) {
+ return true
+ }
+ return PersonalizationApiPigeonUtils.deepEquals(toList(), other.toList()) }
+
+ override fun hashCode(): Int = toList().hashCode()
+}
+private open class PersonalizationApiPigeonCodec : StandardMessageCodec() {
+ override fun readValueOfType(type: Byte, buffer: ByteBuffer): Any? {
+ return when (type) {
+ 129.toByte() -> {
+ return (readValue(buffer) as? List)?.let {
+ InitConfig.fromList(it)
+ }
+ }
+ 130.toByte() -> {
+ return (readValue(buffer) as? List)?.let {
+ PurchaseLineItemWire.fromList(it)
+ }
+ }
+ 131.toByte() -> {
+ return (readValue(buffer) as? List)?.let {
+ ProfileParamsWire.fromList(it)
+ }
+ }
+ else -> super.readValueOfType(type, buffer)
+ }
+ }
+ override fun writeValue(stream: ByteArrayOutputStream, value: Any?) {
+ when (value) {
+ is InitConfig -> {
+ stream.write(129)
+ writeValue(stream, value.toList())
+ }
+ is PurchaseLineItemWire -> {
+ stream.write(130)
+ writeValue(stream, value.toList())
+ }
+ is ProfileParamsWire -> {
+ stream.write(131)
+ writeValue(stream, value.toList())
+ }
+ else -> super.writeValue(stream, value)
+ }
+ }
+}
+
+
+/** Generated interface from Pigeon that represents a handler of messages from Flutter. */
+interface PersonalizationHostApi {
+ fun initialize(config: InitConfig, callback: (Result) -> Unit)
+ fun getPlatformVersion(): String
+ /** Returns the push token stored by the native SDK (if any). */
+ fun getStoredPushToken(): String?
+ /** [customFieldsJson] is JSON object string or null (maps to native custom fields map). */
+ fun trackEvent(event: String, time: Long?, category: String?, label: String?, value: Long?, customFieldsJson: String?, callback: (Result) -> Unit)
+ fun setProfile(params: ProfileParamsWire, callback: (Result) -> Unit)
+ /**
+ * Returns the recommendation block as a JSON string.
+ * [paramsJson] is a JSON object string with optional filter parameters.
+ * Dart layer parses the result into [RecommendationResponse].
+ */
+ fun getRecommendation(code: String, paramsJson: String?, callback: (Result) -> Unit)
+ /** Returns the current session ID from the native SDK. */
+ fun getSid(): String
+ /** Returns the device ID assigned by the native SDK, or null before first sync. */
+ fun getDid(): String?
+ /**
+ * Returns a single product's details as a JSON string.
+ * Dart layer parses the result into [Product].
+ */
+ fun getProductInfo(itemId: String, callback: (Result) -> Unit)
+ /**
+ * Returns a paginated product catalog list as a JSON string.
+ * [paramsJson] is a JSON object with optional filter fields.
+ * Dart layer parses the result into [ProductsListResponse].
+ */
+ fun getProductsList(paramsJson: String?, callback: (Result) -> Unit)
+ /**
+ * Returns blank search results (trending/popular) as a JSON string.
+ * No parameters — the native SDK decides what to return based on shop config.
+ * Dart layer parses the result into [SearchBlankResponse].
+ */
+ fun searchBlank(callback: (Result) -> Unit)
+ /**
+ * Returns instant (typeahead) search results as a JSON string.
+ * [paramsJson] may contain optional "locations" (String) and "excluded_brands" ([String]).
+ * Dart layer parses the result into [SearchInstantResponse].
+ */
+ fun searchInstant(query: String, paramsJson: String?, callback: (Result) -> Unit)
+ /**
+ * Returns full search results as a JSON string.
+ * [paramsJson] is a JSON object string with optional search parameters.
+ * Dart layer parses the result into [SearchFullResponse].
+ */
+ fun searchFull(query: String, paramsJson: String?, callback: (Result) -> Unit)
+ /** [customJson] and [recommendedSourceJson] are JSON object strings or null. */
+ fun trackPurchase(orderId: String, orderPrice: Double, items: List, deliveryType: String?, deliveryAddress: String?, paymentType: String?, isTaxFree: Boolean, promocode: String?, orderCash: Double?, orderBonuses: Double?, orderDelivery: Double?, orderDiscount: Double?, channel: String?, customJson: String?, recommendedSourceJson: String?, stream: String?, segment: String?, callback: (Result) -> Unit)
+
+ companion object {
+ /** The codec used by PersonalizationHostApi. */
+ val codec: MessageCodec by lazy {
+ PersonalizationApiPigeonCodec()
+ }
+ /** Sets up an instance of `PersonalizationHostApi` to handle messages through the `binaryMessenger`. */
+ @JvmOverloads
+ fun setUp(binaryMessenger: BinaryMessenger, api: PersonalizationHostApi?, messageChannelSuffix: String = "") {
+ val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
+ run {
+ val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.personalization_flutter_sdk.PersonalizationHostApi.initialize$separatedMessageChannelSuffix", codec)
+ if (api != null) {
+ channel.setMessageHandler { message, reply ->
+ val args = message as List
+ val configArg = args[0] as InitConfig
+ api.initialize(configArg) { result: Result ->
+ val error = result.exceptionOrNull()
+ if (error != null) {
+ reply.reply(PersonalizationApiPigeonUtils.wrapError(error))
+ } else {
+ reply.reply(PersonalizationApiPigeonUtils.wrapResult(null))
+ }
+ }
+ }
+ } else {
+ channel.setMessageHandler(null)
+ }
+ }
+ run {
+ val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.personalization_flutter_sdk.PersonalizationHostApi.getPlatformVersion$separatedMessageChannelSuffix", codec)
+ if (api != null) {
+ channel.setMessageHandler { _, reply ->
+ val wrapped: List = try {
+ listOf(api.getPlatformVersion())
+ } catch (exception: Throwable) {
+ PersonalizationApiPigeonUtils.wrapError(exception)
+ }
+ reply.reply(wrapped)
+ }
+ } else {
+ channel.setMessageHandler(null)
+ }
+ }
+ run {
+ val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.personalization_flutter_sdk.PersonalizationHostApi.getStoredPushToken$separatedMessageChannelSuffix", codec)
+ if (api != null) {
+ channel.setMessageHandler { _, reply ->
+ val wrapped: List = try {
+ listOf(api.getStoredPushToken())
+ } catch (exception: Throwable) {
+ PersonalizationApiPigeonUtils.wrapError(exception)
+ }
+ reply.reply(wrapped)
+ }
+ } else {
+ channel.setMessageHandler(null)
+ }
+ }
+ run {
+ val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.personalization_flutter_sdk.PersonalizationHostApi.trackEvent$separatedMessageChannelSuffix", codec)
+ if (api != null) {
+ channel.setMessageHandler { message, reply ->
+ val args = message as List
+ val eventArg = args[0] as String
+ val timeArg = args[1] as Long?
+ val categoryArg = args[2] as String?
+ val labelArg = args[3] as String?
+ val valueArg = args[4] as Long?
+ val customFieldsJsonArg = args[5] as String?
+ api.trackEvent(eventArg, timeArg, categoryArg, labelArg, valueArg, customFieldsJsonArg) { result: Result ->
+ val error = result.exceptionOrNull()
+ if (error != null) {
+ reply.reply(PersonalizationApiPigeonUtils.wrapError(error))
+ } else {
+ reply.reply(PersonalizationApiPigeonUtils.wrapResult(null))
+ }
+ }
+ }
+ } else {
+ channel.setMessageHandler(null)
+ }
+ }
+ run {
+ val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.personalization_flutter_sdk.PersonalizationHostApi.setProfile$separatedMessageChannelSuffix", codec)
+ if (api != null) {
+ channel.setMessageHandler { message, reply ->
+ val args = message as List
+ val paramsArg = args[0] as ProfileParamsWire
+ api.setProfile(paramsArg) { result: Result ->
+ val error = result.exceptionOrNull()
+ if (error != null) {
+ reply.reply(PersonalizationApiPigeonUtils.wrapError(error))
+ } else {
+ reply.reply(PersonalizationApiPigeonUtils.wrapResult(null))
+ }
+ }
+ }
+ } else {
+ channel.setMessageHandler(null)
+ }
+ }
+ run {
+ val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.personalization_flutter_sdk.PersonalizationHostApi.getRecommendation$separatedMessageChannelSuffix", codec)
+ if (api != null) {
+ channel.setMessageHandler { message, reply ->
+ val args = message as List
+ val codeArg = args[0] as String
+ val paramsJsonArg = args[1] as String?
+ api.getRecommendation(codeArg, paramsJsonArg) { result: Result ->
+ val error = result.exceptionOrNull()
+ if (error != null) {
+ reply.reply(PersonalizationApiPigeonUtils.wrapError(error))
+ } else {
+ val data = result.getOrNull()
+ reply.reply(PersonalizationApiPigeonUtils.wrapResult(data))
+ }
+ }
+ }
+ } else {
+ channel.setMessageHandler(null)
+ }
+ }
+ run {
+ val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.personalization_flutter_sdk.PersonalizationHostApi.getSid$separatedMessageChannelSuffix", codec)
+ if (api != null) {
+ channel.setMessageHandler { _, reply ->
+ val wrapped: List = try {
+ listOf(api.getSid())
+ } catch (exception: Throwable) {
+ PersonalizationApiPigeonUtils.wrapError(exception)
+ }
+ reply.reply(wrapped)
+ }
+ } else {
+ channel.setMessageHandler(null)
+ }
+ }
+ run {
+ val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.personalization_flutter_sdk.PersonalizationHostApi.getDid$separatedMessageChannelSuffix", codec)
+ if (api != null) {
+ channel.setMessageHandler { _, reply ->
+ val wrapped: List = try {
+ listOf(api.getDid())
+ } catch (exception: Throwable) {
+ PersonalizationApiPigeonUtils.wrapError(exception)
+ }
+ reply.reply(wrapped)
+ }
+ } else {
+ channel.setMessageHandler(null)
+ }
+ }
+ run {
+ val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.personalization_flutter_sdk.PersonalizationHostApi.getProductInfo$separatedMessageChannelSuffix", codec)
+ if (api != null) {
+ channel.setMessageHandler { message, reply ->
+ val args = message as List
+ val itemIdArg = args[0] as String
+ api.getProductInfo(itemIdArg) { result: Result ->
+ val error = result.exceptionOrNull()
+ if (error != null) {
+ reply.reply(PersonalizationApiPigeonUtils.wrapError(error))
+ } else {
+ val data = result.getOrNull()
+ reply.reply(PersonalizationApiPigeonUtils.wrapResult(data))
+ }
+ }
+ }
+ } else {
+ channel.setMessageHandler(null)
+ }
+ }
+ run {
+ val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.personalization_flutter_sdk.PersonalizationHostApi.getProductsList$separatedMessageChannelSuffix", codec)
+ if (api != null) {
+ channel.setMessageHandler { message, reply ->
+ val args = message as List
+ val paramsJsonArg = args[0] as String?
+ api.getProductsList(paramsJsonArg) { result: Result ->
+ val error = result.exceptionOrNull()
+ if (error != null) {
+ reply.reply(PersonalizationApiPigeonUtils.wrapError(error))
+ } else {
+ val data = result.getOrNull()
+ reply.reply(PersonalizationApiPigeonUtils.wrapResult(data))
+ }
+ }
+ }
+ } else {
+ channel.setMessageHandler(null)
+ }
+ }
+ run {
+ val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.personalization_flutter_sdk.PersonalizationHostApi.searchBlank$separatedMessageChannelSuffix", codec)
+ if (api != null) {
+ channel.setMessageHandler { _, reply ->
+ api.searchBlank{ result: Result ->
+ val error = result.exceptionOrNull()
+ if (error != null) {
+ reply.reply(PersonalizationApiPigeonUtils.wrapError(error))
+ } else {
+ val data = result.getOrNull()
+ reply.reply(PersonalizationApiPigeonUtils.wrapResult(data))
+ }
+ }
+ }
+ } else {
+ channel.setMessageHandler(null)
+ }
+ }
+ run {
+ val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.personalization_flutter_sdk.PersonalizationHostApi.searchInstant$separatedMessageChannelSuffix", codec)
+ if (api != null) {
+ channel.setMessageHandler { message, reply ->
+ val args = message as List
+ val queryArg = args[0] as String
+ val paramsJsonArg = args[1] as String?
+ api.searchInstant(queryArg, paramsJsonArg) { result: Result ->
+ val error = result.exceptionOrNull()
+ if (error != null) {
+ reply.reply(PersonalizationApiPigeonUtils.wrapError(error))
+ } else {
+ val data = result.getOrNull()
+ reply.reply(PersonalizationApiPigeonUtils.wrapResult(data))
+ }
+ }
+ }
+ } else {
+ channel.setMessageHandler(null)
+ }
+ }
+ run {
+ val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.personalization_flutter_sdk.PersonalizationHostApi.searchFull$separatedMessageChannelSuffix", codec)
+ if (api != null) {
+ channel.setMessageHandler { message, reply ->
+ val args = message as List
+ val queryArg = args[0] as String
+ val paramsJsonArg = args[1] as String?
+ api.searchFull(queryArg, paramsJsonArg) { result: Result ->
+ val error = result.exceptionOrNull()
+ if (error != null) {
+ reply.reply(PersonalizationApiPigeonUtils.wrapError(error))
+ } else {
+ val data = result.getOrNull()
+ reply.reply(PersonalizationApiPigeonUtils.wrapResult(data))
+ }
+ }
+ }
+ } else {
+ channel.setMessageHandler(null)
+ }
+ }
+ run {
+ val channel = BasicMessageChannel(binaryMessenger, "dev.flutter.pigeon.personalization_flutter_sdk.PersonalizationHostApi.trackPurchase$separatedMessageChannelSuffix", codec)
+ if (api != null) {
+ channel.setMessageHandler { message, reply ->
+ val args = message as List
+ val orderIdArg = args[0] as String
+ val orderPriceArg = args[1] as Double
+ val itemsArg = args[2] as List
+ val deliveryTypeArg = args[3] as String?
+ val deliveryAddressArg = args[4] as String?
+ val paymentTypeArg = args[5] as String?
+ val isTaxFreeArg = args[6] as Boolean
+ val promocodeArg = args[7] as String?
+ val orderCashArg = args[8] as Double?
+ val orderBonusesArg = args[9] as Double?
+ val orderDeliveryArg = args[10] as Double?
+ val orderDiscountArg = args[11] as Double?
+ val channelArg = args[12] as String?
+ val customJsonArg = args[13] as String?
+ val recommendedSourceJsonArg = args[14] as String?
+ val streamArg = args[15] as String?
+ val segmentArg = args[16] as String?
+ api.trackPurchase(orderIdArg, orderPriceArg, itemsArg, deliveryTypeArg, deliveryAddressArg, paymentTypeArg, isTaxFreeArg, promocodeArg, orderCashArg, orderBonusesArg, orderDeliveryArg, orderDiscountArg, channelArg, customJsonArg, recommendedSourceJsonArg, streamArg, segmentArg) { result: Result ->
+ val error = result.exceptionOrNull()
+ if (error != null) {
+ reply.reply(PersonalizationApiPigeonUtils.wrapError(error))
+ } else {
+ reply.reply(PersonalizationApiPigeonUtils.wrapResult(null))
+ }
+ }
+ }
+ } else {
+ channel.setMessageHandler(null)
+ }
+ }
+ }
+ }
+}
+/** Generated class from Pigeon that represents Flutter messages that can be called from Kotlin. */
+class PersonalizationFlutterApi(private val binaryMessenger: BinaryMessenger, private val messageChannelSuffix: String = "") {
+ companion object {
+ /** The codec used by PersonalizationFlutterApi. */
+ val codec: MessageCodec by lazy {
+ PersonalizationApiPigeonCodec()
+ }
+ }
+ fun onPushReceived(payloadArg: Map, callback: (Result) -> Unit)
+{
+ val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
+ val channelName = "dev.flutter.pigeon.personalization_flutter_sdk.PersonalizationFlutterApi.onPushReceived$separatedMessageChannelSuffix"
+ val channel = BasicMessageChannel(binaryMessenger, channelName, codec)
+ channel.send(listOf(payloadArg)) {
+ if (it is List<*>) {
+ if (it.size > 1) {
+ callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?)))
+ } else {
+ callback(Result.success(Unit))
+ }
+ } else {
+ callback(Result.failure(PersonalizationApiPigeonUtils.createConnectionError(channelName)))
+ }
+ }
+ }
+ fun onPushDelivered(payloadArg: Map, callback: (Result) -> Unit)
+{
+ val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
+ val channelName = "dev.flutter.pigeon.personalization_flutter_sdk.PersonalizationFlutterApi.onPushDelivered$separatedMessageChannelSuffix"
+ val channel = BasicMessageChannel(binaryMessenger, channelName, codec)
+ channel.send(listOf(payloadArg)) {
+ if (it is List<*>) {
+ if (it.size > 1) {
+ callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?)))
+ } else {
+ callback(Result.success(Unit))
+ }
+ } else {
+ callback(Result.failure(PersonalizationApiPigeonUtils.createConnectionError(channelName)))
+ }
+ }
+ }
+ fun onPushClicked(payloadArg: Map, callback: (Result) -> Unit)
+{
+ val separatedMessageChannelSuffix = if (messageChannelSuffix.isNotEmpty()) ".$messageChannelSuffix" else ""
+ val channelName = "dev.flutter.pigeon.personalization_flutter_sdk.PersonalizationFlutterApi.onPushClicked$separatedMessageChannelSuffix"
+ val channel = BasicMessageChannel(binaryMessenger, channelName, codec)
+ channel.send(listOf(payloadArg)) {
+ if (it is List<*>) {
+ if (it.size > 1) {
+ callback(Result.failure(FlutterError(it[0] as String, it[1] as String, it[2] as String?)))
+ } else {
+ callback(Result.success(Unit))
+ }
+ } else {
+ callback(Result.failure(PersonalizationApiPigeonUtils.createConnectionError(channelName)))
+ }
+ }
+ }
+}
diff --git a/android/src/test/kotlin/com/rees46/rees46_flutter_sdk/Rees46FlutterSdkPluginTest.kt b/android/src/test/kotlin/com/rees46/rees46_flutter_sdk/Rees46FlutterSdkPluginTest.kt
new file mode 100644
index 0000000..256c0a5
--- /dev/null
+++ b/android/src/test/kotlin/com/rees46/rees46_flutter_sdk/Rees46FlutterSdkPluginTest.kt
@@ -0,0 +1,20 @@
+package com.rees46.rees46_flutter_sdk
+
+import kotlin.test.Test
+import kotlin.test.assertTrue
+
+/*
+ * This demonstrates a simple unit test of the Kotlin portion of this plugin's implementation.
+ *
+ * Once you have built the plugin's example app, you can run these tests from the command
+ * line by running `./gradlew testDebugUnitTest` in the `example/android/` directory, or
+ * you can run them directly from IDEs that support JUnit such as Android Studio.
+ */
+
+internal class Rees46FlutterSdkPluginTest {
+ @Test
+ fun getPlatformVersion_containsAndroidWord() {
+ val plugin = Rees46FlutterSdkPlugin()
+ assertTrue(plugin.getPlatformVersion().startsWith("Android "))
+ }
+}
diff --git a/example/.gitignore b/example/.gitignore
new file mode 100644
index 0000000..3820a95
--- /dev/null
+++ b/example/.gitignore
@@ -0,0 +1,45 @@
+# Miscellaneous
+*.class
+*.log
+*.pyc
+*.swp
+.DS_Store
+.atom/
+.build/
+.buildlog/
+.history
+.svn/
+.swiftpm/
+migrate_working_dir/
+
+# IntelliJ related
+*.iml
+*.ipr
+*.iws
+.idea/
+
+# The .vscode folder contains launch configuration and tasks you configure in
+# VS Code which you may wish to be included in version control, so this line
+# is commented out by default.
+#.vscode/
+
+# Flutter/Dart/Pub related
+**/doc/api/
+**/ios/Flutter/.last_build_id
+.dart_tool/
+.flutter-plugins-dependencies
+.pub-cache/
+.pub/
+/build/
+/coverage/
+
+# Symbolication related
+app.*.symbols
+
+# Obfuscation related
+app.*.map.json
+
+# Android Studio will place build artifacts here
+/android/app/debug
+/android/app/profile
+/android/app/release
diff --git a/example/README.md b/example/README.md
new file mode 100644
index 0000000..c106ba1
--- /dev/null
+++ b/example/README.md
@@ -0,0 +1,17 @@
+# rees46_flutter_sdk_example
+
+Demonstrates how to use the rees46_flutter_sdk plugin.
+
+## Getting Started
+
+This project is a starting point for a Flutter application.
+
+A few resources to get you started if this is your first Flutter project:
+
+- [Learn Flutter](https://docs.flutter.dev/get-started/learn-flutter)
+- [Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
+- [Flutter learning resources](https://docs.flutter.dev/reference/learning-resources)
+
+For help getting started with Flutter development, view the
+[online documentation](https://docs.flutter.dev/), which offers tutorials,
+samples, guidance on mobile development, and a full API reference.
diff --git a/example/analysis_options.yaml b/example/analysis_options.yaml
new file mode 100644
index 0000000..0d29021
--- /dev/null
+++ b/example/analysis_options.yaml
@@ -0,0 +1,28 @@
+# This file configures the analyzer, which statically analyzes Dart code to
+# check for errors, warnings, and lints.
+#
+# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
+# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
+# invoked from the command line by running `flutter analyze`.
+
+# The following line activates a set of recommended lints for Flutter apps,
+# packages, and plugins designed to encourage good coding practices.
+include: package:flutter_lints/flutter.yaml
+
+linter:
+ # The lint rules applied to this project can be customized in the
+ # section below to disable rules from the `package:flutter_lints/flutter.yaml`
+ # included above or to enable additional rules. A list of all available lints
+ # and their documentation is published at https://dart.dev/lints.
+ #
+ # Instead of disabling a lint rule for the entire project in the
+ # section below, it can also be suppressed for a single line of code
+ # or a specific dart file by using the `// ignore: name_of_lint` and
+ # `// ignore_for_file: name_of_lint` syntax on the line or in the file
+ # producing the lint.
+ rules:
+ # avoid_print: false # Uncomment to disable the `avoid_print` rule
+ # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
+
+# Additional information about this file can be found at
+# https://dart.dev/guides/language/analysis-options
diff --git a/example/android/.gitignore b/example/android/.gitignore
new file mode 100644
index 0000000..be3943c
--- /dev/null
+++ b/example/android/.gitignore
@@ -0,0 +1,14 @@
+gradle-wrapper.jar
+/.gradle
+/captures/
+/gradlew
+/gradlew.bat
+/local.properties
+GeneratedPluginRegistrant.java
+.cxx/
+
+# Remember to never publicly share your keystore.
+# See https://flutter.dev/to/reference-keystore
+key.properties
+**/*.keystore
+**/*.jks
diff --git a/example/android/app/build.gradle.kts b/example/android/app/build.gradle.kts
new file mode 100644
index 0000000..3c4b1ed
--- /dev/null
+++ b/example/android/app/build.gradle.kts
@@ -0,0 +1,58 @@
+plugins {
+ id("com.android.application")
+ id("kotlin-android")
+ // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
+ id("dev.flutter.flutter-gradle-plugin")
+ // Processes google-services.json so the native SDK's FCM (firebase-messaging,
+ // pulled transitively by rees46-sdk) can auto-initialize FirebaseApp.
+ id("com.google.gms.google-services")
+}
+
+android {
+ namespace = "com.rees46.rees46_flutter_sdk_example"
+ compileSdk = flutter.compileSdkVersion
+ ndkVersion = flutter.ndkVersion
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+
+ kotlinOptions {
+ jvmTarget = JavaVersion.VERSION_17.toString()
+ }
+
+ defaultConfig {
+ // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
+ applicationId = "com.rees46.rees46_flutter_sdk_example"
+ // You can update the following values to match your application needs.
+ // For more information, see: https://flutter.dev/to/review-gradle-config.
+ minSdk = flutter.minSdkVersion
+ targetSdk = flutter.targetSdkVersion
+ versionCode = flutter.versionCode
+ versionName = flutter.versionName
+
+ testInstrumentationRunner = "pl.leancode.patrol.PatrolJUnitRunner"
+ testInstrumentationRunnerArguments["clearPackageData"] = "true"
+ }
+
+ testOptions {
+ execution = "ANDROIDX_TEST_ORCHESTRATOR"
+ }
+
+ buildTypes {
+ release {
+ // TODO: Add your own signing config for the release build.
+ // Signing with the debug keys for now, so `flutter run --release` works.
+ signingConfig = signingConfigs.getByName("debug")
+ }
+ }
+}
+
+flutter {
+ source = "../.."
+}
+
+dependencies {
+ androidTestUtil("androidx.test:orchestrator:1.5.1")
+}
diff --git a/example/android/app/google-services.json b/example/android/app/google-services.json
new file mode 100644
index 0000000..f292eea
--- /dev/null
+++ b/example/android/app/google-services.json
@@ -0,0 +1,238 @@
+{
+ "project_info": {
+ "project_number": "605730184710",
+ "project_id": "rees46-com",
+ "storage_bucket": "rees46-com.firebasestorage.app"
+ },
+ "client": [
+ {
+ "client_info": {
+ "mobilesdk_app_id": "1:605730184710:android:6c640baac4dda9e05b6d87",
+ "android_client_info": {
+ "package_name": "com.demoapp"
+ }
+ },
+ "oauth_client": [],
+ "api_key": [
+ {
+ "current_key": "AIzaSyAMI5DiiCqeZ2JV2m6r0Be520N39WKh_TA"
+ }
+ ],
+ "services": {
+ "appinvite_service": {
+ "other_platform_oauth_client": []
+ }
+ }
+ },
+ {
+ "client_info": {
+ "mobilesdk_app_id": "1:605730184710:android:7b6eb053665460b55b6d87",
+ "android_client_info": {
+ "package_name": "com.example.rees46_java_app"
+ }
+ },
+ "oauth_client": [],
+ "api_key": [
+ {
+ "current_key": "AIzaSyAMI5DiiCqeZ2JV2m6r0Be520N39WKh_TA"
+ }
+ ],
+ "services": {
+ "appinvite_service": {
+ "other_platform_oauth_client": []
+ }
+ }
+ },
+ {
+ "client_info": {
+ "mobilesdk_app_id": "1:605730184710:android:198526dca19b16ec5b6d87",
+ "android_client_info": {
+ "package_name": "com.personaclick.sample"
+ }
+ },
+ "oauth_client": [],
+ "api_key": [
+ {
+ "current_key": "AIzaSyAMI5DiiCqeZ2JV2m6r0Be520N39WKh_TA"
+ }
+ ],
+ "services": {
+ "appinvite_service": {
+ "other_platform_oauth_client": []
+ }
+ }
+ },
+ {
+ "client_info": {
+ "mobilesdk_app_id": "1:605730184710:android:e3fd667d60c9bdb75b6d87",
+ "android_client_info": {
+ "package_name": "com.personalizatio.sample"
+ }
+ },
+ "oauth_client": [],
+ "api_key": [
+ {
+ "current_key": "AIzaSyAMI5DiiCqeZ2JV2m6r0Be520N39WKh_TA"
+ }
+ ],
+ "services": {
+ "appinvite_service": {
+ "other_platform_oauth_client": []
+ }
+ }
+ },
+ {
+ "client_info": {
+ "mobilesdk_app_id": "1:605730184710:android:2068bac26248342c5b6d87",
+ "android_client_info": {
+ "package_name": "com.personalization.demo"
+ }
+ },
+ "oauth_client": [],
+ "api_key": [
+ {
+ "current_key": "AIzaSyAMI5DiiCqeZ2JV2m6r0Be520N39WKh_TA"
+ }
+ ],
+ "services": {
+ "appinvite_service": {
+ "other_platform_oauth_client": []
+ }
+ }
+ },
+ {
+ "client_info": {
+ "mobilesdk_app_id": "1:605730184710:android:f4dd23bdbd308bb05b6d87",
+ "android_client_info": {
+ "package_name": "com.personalization.sample"
+ }
+ },
+ "oauth_client": [],
+ "api_key": [
+ {
+ "current_key": "AIzaSyAMI5DiiCqeZ2JV2m6r0Be520N39WKh_TA"
+ }
+ ],
+ "services": {
+ "appinvite_service": {
+ "other_platform_oauth_client": []
+ }
+ }
+ },
+ {
+ "client_info": {
+ "mobilesdk_app_id": "1:605730184710:android:60bfd61d91f6b9e15b6d87",
+ "android_client_info": {
+ "package_name": "com.reactnativedevapp"
+ }
+ },
+ "oauth_client": [],
+ "api_key": [
+ {
+ "current_key": "AIzaSyAMI5DiiCqeZ2JV2m6r0Be520N39WKh_TA"
+ }
+ ],
+ "services": {
+ "appinvite_service": {
+ "other_platform_oauth_client": []
+ }
+ }
+ },
+ {
+ "client_info": {
+ "mobilesdk_app_id": "1:605730184710:android:ba86623dfc47ff155b6d87",
+ "android_client_info": {
+ "package_name": "com.rees46.loyaltyProgram"
+ }
+ },
+ "oauth_client": [],
+ "api_key": [
+ {
+ "current_key": "AIzaSyAMI5DiiCqeZ2JV2m6r0Be520N39WKh_TA"
+ }
+ ],
+ "services": {
+ "appinvite_service": {
+ "other_platform_oauth_client": []
+ }
+ }
+ },
+ {
+ "client_info": {
+ "mobilesdk_app_id": "1:605730184710:android:beded358e1393fe55b6d87",
+ "android_client_info": {
+ "package_name": "com.rees46.rees46_flutter_sdk_example"
+ }
+ },
+ "oauth_client": [],
+ "api_key": [
+ {
+ "current_key": "AIzaSyAMI5DiiCqeZ2JV2m6r0Be520N39WKh_TA"
+ }
+ ],
+ "services": {
+ "appinvite_service": {
+ "other_platform_oauth_client": []
+ }
+ }
+ },
+ {
+ "client_info": {
+ "mobilesdk_app_id": "1:605730184710:android:75abe9965c01c1c45b6d87",
+ "android_client_info": {
+ "package_name": "com.rees46.sample"
+ }
+ },
+ "oauth_client": [],
+ "api_key": [
+ {
+ "current_key": "AIzaSyAMI5DiiCqeZ2JV2m6r0Be520N39WKh_TA"
+ }
+ ],
+ "services": {
+ "appinvite_service": {
+ "other_platform_oauth_client": []
+ }
+ }
+ },
+ {
+ "client_info": {
+ "mobilesdk_app_id": "1:605730184710:android:9f9c2bc678183a005b6d87",
+ "android_client_info": {
+ "package_name": "com.sdkdev"
+ }
+ },
+ "oauth_client": [],
+ "api_key": [
+ {
+ "current_key": "AIzaSyAMI5DiiCqeZ2JV2m6r0Be520N39WKh_TA"
+ }
+ ],
+ "services": {
+ "appinvite_service": {
+ "other_platform_oauth_client": []
+ }
+ }
+ },
+ {
+ "client_info": {
+ "mobilesdk_app_id": "1:605730184710:android:4cf54376b6a2868e5b6d87",
+ "android_client_info": {
+ "package_name": "rees46.demo_android"
+ }
+ },
+ "oauth_client": [],
+ "api_key": [
+ {
+ "current_key": "AIzaSyAMI5DiiCqeZ2JV2m6r0Be520N39WKh_TA"
+ }
+ ],
+ "services": {
+ "appinvite_service": {
+ "other_platform_oauth_client": []
+ }
+ }
+ }
+ ],
+ "configuration_version": "1"
+}
diff --git a/example/android/app/src/androidTest/java/com/rees46/rees46_flutter_sdk_example/MainActivityTest.java b/example/android/app/src/androidTest/java/com/rees46/rees46_flutter_sdk_example/MainActivityTest.java
new file mode 100644
index 0000000..c9993ce
--- /dev/null
+++ b/example/android/app/src/androidTest/java/com/rees46/rees46_flutter_sdk_example/MainActivityTest.java
@@ -0,0 +1,32 @@
+package com.rees46.rees46_flutter_sdk_example;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+import pl.leancode.patrol.PatrolJUnitRunner;
+
+@RunWith(Parameterized.class)
+public class MainActivityTest {
+ @Parameters(name = "{0}")
+ public static Object[] testCases() {
+ PatrolJUnitRunner instrumentation = (PatrolJUnitRunner) InstrumentationRegistry.getInstrumentation();
+ instrumentation.setUp(MainActivity.class);
+ instrumentation.waitForPatrolAppService();
+ return instrumentation.listDartTests();
+ }
+
+ public MainActivityTest(String dartTestName) {
+ this.dartTestName = dartTestName;
+ }
+
+ private final String dartTestName;
+
+ @Test
+ public void runDartTest() {
+ PatrolJUnitRunner instrumentation = (PatrolJUnitRunner) InstrumentationRegistry.getInstrumentation();
+ instrumentation.runDartTest(dartTestName);
+ }
+}
+
diff --git a/example/android/app/src/debug/AndroidManifest.xml b/example/android/app/src/debug/AndroidManifest.xml
new file mode 100644
index 0000000..399f698
--- /dev/null
+++ b/example/android/app/src/debug/AndroidManifest.xml
@@ -0,0 +1,7 @@
+
+
+
+
diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..beb2859
--- /dev/null
+++ b/example/android/app/src/main/AndroidManifest.xml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/example/android/app/src/main/kotlin/com/rees46/rees46_flutter_sdk_example/MainActivity.kt b/example/android/app/src/main/kotlin/com/rees46/rees46_flutter_sdk_example/MainActivity.kt
new file mode 100644
index 0000000..e9ff4da
--- /dev/null
+++ b/example/android/app/src/main/kotlin/com/rees46/rees46_flutter_sdk_example/MainActivity.kt
@@ -0,0 +1,5 @@
+package com.rees46.rees46_flutter_sdk_example
+
+import io.flutter.embedding.android.FlutterActivity
+
+class MainActivity : FlutterActivity()
diff --git a/example/android/app/src/main/res/drawable-v21/launch_background.xml b/example/android/app/src/main/res/drawable-v21/launch_background.xml
new file mode 100644
index 0000000..f74085f
--- /dev/null
+++ b/example/android/app/src/main/res/drawable-v21/launch_background.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
diff --git a/example/android/app/src/main/res/drawable/launch_background.xml b/example/android/app/src/main/res/drawable/launch_background.xml
new file mode 100644
index 0000000..304732f
--- /dev/null
+++ b/example/android/app/src/main/res/drawable/launch_background.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
diff --git a/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..db77bb4
Binary files /dev/null and b/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..17987b7
Binary files /dev/null and b/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..09d4391
Binary files /dev/null and b/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..d5f1c8d
Binary files /dev/null and b/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..4d6372e
Binary files /dev/null and b/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/example/android/app/src/main/res/values-night/styles.xml b/example/android/app/src/main/res/values-night/styles.xml
new file mode 100644
index 0000000..06952be
--- /dev/null
+++ b/example/android/app/src/main/res/values-night/styles.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
diff --git a/example/android/app/src/main/res/values/styles.xml b/example/android/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000..cb1ef88
--- /dev/null
+++ b/example/android/app/src/main/res/values/styles.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
diff --git a/example/android/app/src/profile/AndroidManifest.xml b/example/android/app/src/profile/AndroidManifest.xml
new file mode 100644
index 0000000..399f698
--- /dev/null
+++ b/example/android/app/src/profile/AndroidManifest.xml
@@ -0,0 +1,7 @@
+
+
+
+
diff --git a/example/android/build.gradle.kts b/example/android/build.gradle.kts
new file mode 100644
index 0000000..eeea458
--- /dev/null
+++ b/example/android/build.gradle.kts
@@ -0,0 +1,25 @@
+allprojects {
+ repositories {
+ google()
+ mavenCentral()
+ maven(url = "https://jitpack.io")
+ }
+}
+
+val newBuildDir: Directory =
+ rootProject.layout.buildDirectory
+ .dir("../../build")
+ .get()
+rootProject.layout.buildDirectory.value(newBuildDir)
+
+subprojects {
+ val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
+ project.layout.buildDirectory.value(newSubprojectBuildDir)
+}
+subprojects {
+ project.evaluationDependsOn(":app")
+}
+
+tasks.register("clean") {
+ delete(rootProject.layout.buildDirectory)
+}
diff --git a/example/android/gradle.properties b/example/android/gradle.properties
new file mode 100644
index 0000000..fbee1d8
--- /dev/null
+++ b/example/android/gradle.properties
@@ -0,0 +1,2 @@
+org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
+android.useAndroidX=true
diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..e4ef43f
--- /dev/null
+++ b/example/android/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip
diff --git a/example/android/settings.gradle.kts b/example/android/settings.gradle.kts
new file mode 100644
index 0000000..51a76cd
--- /dev/null
+++ b/example/android/settings.gradle.kts
@@ -0,0 +1,27 @@
+pluginManagement {
+ val flutterSdkPath =
+ run {
+ val properties = java.util.Properties()
+ file("local.properties").inputStream().use { properties.load(it) }
+ val flutterSdkPath = properties.getProperty("flutter.sdk")
+ require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
+ flutterSdkPath
+ }
+
+ includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
+
+ repositories {
+ google()
+ mavenCentral()
+ gradlePluginPortal()
+ }
+}
+
+plugins {
+ id("dev.flutter.flutter-plugin-loader") version "1.0.0"
+ id("com.android.application") version "8.11.1" apply false
+ id("org.jetbrains.kotlin.android") version "2.2.20" apply false
+ id("com.google.gms.google-services") version "4.4.3" apply false
+}
+
+include(":app")
diff --git a/example/integration_test/init_flow_test.dart b/example/integration_test/init_flow_test.dart
new file mode 100644
index 0000000..85a2351
--- /dev/null
+++ b/example/integration_test/init_flow_test.dart
@@ -0,0 +1,47 @@
+import 'package:flutter/widgets.dart';
+import 'package:patrol/patrol.dart';
+
+import 'package:personalization_flutter_sdk_example/main.dart' as app;
+
+void main() {
+ patrolTest('auto-initializes on startup with hardcoded config', ($) async {
+ await $.pumpWidgetAndSettle(const app.App());
+
+ await $(
+ 'Status: Initialized',
+ ).waitUntilVisible(timeout: const Duration(seconds: 30));
+ });
+
+ patrolTest('tracking buttons are visible after auto-initialization', (
+ $,
+ ) async {
+ await $.pumpWidgetAndSettle(const app.App());
+
+ await $(
+ 'Status: Initialized',
+ ).waitUntilVisible(timeout: const Duration(seconds: 30));
+ await $('Send demo trackEvent').scrollTo();
+ await $('Send demo trackPurchase').scrollTo();
+ });
+
+ patrolTest(
+ 'Re-initialize button re-runs initialization and returns to Initialized',
+ ($) async {
+ await $.pumpWidgetAndSettle(const app.App());
+
+ await $(
+ 'Status: Initialized',
+ ).waitUntilVisible(timeout: const Duration(seconds: 30));
+
+ await $('Re-initialize').scrollTo();
+ await $('Re-initialize').tap();
+
+ await $(
+ 'Status: Initialized',
+ ).waitUntilVisible(timeout: const Duration(seconds: 30));
+ await $(
+ 'Status: Initialized',
+ ).scrollTo(scrollDirection: AxisDirection.up);
+ },
+ );
+}
diff --git a/example/integration_test/patrol_smoke_test.dart b/example/integration_test/patrol_smoke_test.dart
new file mode 100644
index 0000000..bb7c4b5
--- /dev/null
+++ b/example/integration_test/patrol_smoke_test.dart
@@ -0,0 +1,29 @@
+import 'package:patrol/patrol.dart';
+
+import 'package:personalization_flutter_sdk_example/main.dart' as app;
+
+void main() {
+ patrolTest('app launches and all key sections are visible', ($) async {
+ await $.pumpWidgetAndSettle(const app.App());
+
+ await $('REES46 SDK init demo').waitUntilVisible();
+ await $('Initialization').waitUntilVisible();
+ await $('Stored push token').waitUntilVisible();
+ await $('Tracking').scrollTo();
+ await $('Send demo trackEvent').scrollTo();
+ await $('Send demo trackPurchase').scrollTo();
+ });
+
+ patrolTest(
+ 'auto-initializes on startup and exposes the Re-initialize button',
+ ($) async {
+ await $.pumpWidgetAndSettle(const app.App());
+
+ // The demo uses hardcoded config and initializes itself on launch.
+ await $(
+ 'Status: Initialized',
+ ).waitUntilVisible(timeout: const Duration(seconds: 30));
+ await $('Re-initialize').scrollTo();
+ },
+ );
+}
diff --git a/example/integration_test/product_info_sdk_test.dart b/example/integration_test/product_info_sdk_test.dart
new file mode 100644
index 0000000..6253bc8
--- /dev/null
+++ b/example/integration_test/product_info_sdk_test.dart
@@ -0,0 +1,77 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:patrol/patrol.dart';
+
+import 'package:personalization_flutter_sdk_example/main.dart' as app;
+
+import 'test_config.dart';
+
+Future _initializeSdk(PatrolIntegrationTester $) async {
+ await $.pumpWidgetAndSettle(const app.App());
+ await $(
+ 'Status: Initialized',
+ ).waitUntilExists(timeout: const Duration(seconds: 30));
+ await $('Status: Initialized').scrollTo(scrollDirection: AxisDirection.up);
+}
+
+String _labelText(PatrolIntegrationTester $, String key) {
+ final widget = $.tester.widget(find.byKey(Key(key)));
+ return widget.data ?? '';
+}
+
+void main() {
+ patrolTest('getProductInfo — returns product name for valid ID', ($) async {
+ await _initializeSdk($);
+
+ await $.tester.enterText(
+ find.byKey(const Key('field_product_id')),
+ TestConfig.productId,
+ );
+ await $.tester.pump();
+
+ await $('Get Product Info').scrollTo();
+ await $('Get Product Info').tap();
+ await $('Get Product Info').waitUntilVisible();
+
+ expect(find.byKey(const Key('lbl_product_info_error')), findsNothing);
+
+ final nameText = _labelText($, 'lbl_product_info_name');
+ expect(nameText, startsWith('Name:'));
+ expect(nameText.length, greaterThan('Name:'.length));
+ });
+
+ patrolTest('getProductInfo — empty ID is no-op in the UI', ($) async {
+ await _initializeSdk($);
+
+ await $('Get Product Info').scrollTo();
+ await $('Get Product Info').tap();
+ await $.pumpAndSettle();
+
+ expect(find.byKey(const Key('lbl_product_info_name')), findsNothing);
+ expect(find.byKey(const Key('lbl_product_info_error')), findsNothing);
+ });
+
+ patrolTest('getProductInfo — invalid ID shows error', ($) async {
+ await _initializeSdk($);
+
+ await $.tester.enterText(
+ find.byKey(const Key('field_product_id')),
+ 'nonexistent_product_id_xyz',
+ );
+ await $.tester.pump();
+
+ await $('Get Product Info').scrollTo();
+ await $('Get Product Info').tap();
+ await $('Get Product Info').waitUntilVisible();
+
+ final hasName = find
+ .byKey(const Key('lbl_product_info_name'))
+ .evaluate()
+ .isNotEmpty;
+ final hasError = find
+ .byKey(const Key('lbl_product_info_error'))
+ .evaluate()
+ .isNotEmpty;
+ expect(hasName || hasError, isTrue);
+ });
+}
diff --git a/example/integration_test/products_list_sdk_test.dart b/example/integration_test/products_list_sdk_test.dart
new file mode 100644
index 0000000..7f5422a
--- /dev/null
+++ b/example/integration_test/products_list_sdk_test.dart
@@ -0,0 +1,63 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:patrol/patrol.dart';
+
+import 'package:personalization_flutter_sdk_example/main.dart' as app;
+
+Future _initializeSdk(PatrolIntegrationTester $) async {
+ await $.pumpWidgetAndSettle(const app.App());
+ await $(
+ 'Status: Initialized',
+ ).waitUntilExists(timeout: const Duration(seconds: 30));
+ await $('Status: Initialized').scrollTo(scrollDirection: AxisDirection.up);
+}
+
+String _labelText(PatrolIntegrationTester $, String key) {
+ final widget = $.tester.widget(find.byKey(Key(key)));
+ return widget.data ?? '';
+}
+
+void main() {
+ patrolTest('getProductsList — returns non-negative total', ($) async {
+ await _initializeSdk($);
+
+ await $('Get Products List').scrollTo();
+ await $('Get Products List').tap();
+ await $('Get Products List').waitUntilVisible();
+
+ expect(find.byKey(const Key('lbl_products_list_error')), findsNothing);
+
+ final totalText = _labelText($, 'lbl_products_list_total');
+ expect(totalText, startsWith('Total:'));
+ final n = int.tryParse(totalText.replaceFirst('Total: ', ''));
+ expect(n, isNotNull);
+ expect(n, greaterThanOrEqualTo(0));
+ });
+
+ patrolTest('getProductsList — consistent total on repeated calls', ($) async {
+ await _initializeSdk($);
+
+ await $('Get Products List').scrollTo();
+ await $('Get Products List').tap();
+ await $('Get Products List').waitUntilVisible();
+ final first = _labelText($, 'lbl_products_list_total');
+
+ await $('Get Products List').scrollTo();
+ await $('Get Products List').tap();
+ await $('Get Products List').waitUntilVisible();
+ final second = _labelText($, 'lbl_products_list_total');
+
+ expect(first, equals(second));
+ });
+
+ patrolTest('getProductsList — no error after init', ($) async {
+ await _initializeSdk($);
+
+ await $('Get Products List').scrollTo();
+ await $('Get Products List').tap();
+ await $('Get Products List').waitUntilVisible();
+
+ expect(find.byKey(const Key('lbl_products_list_error')), findsNothing);
+ expect(find.byKey(const Key('lbl_products_list_total')), findsOneWidget);
+ });
+}
diff --git a/example/integration_test/profile_sdk_test.dart b/example/integration_test/profile_sdk_test.dart
new file mode 100644
index 0000000..31c3404
--- /dev/null
+++ b/example/integration_test/profile_sdk_test.dart
@@ -0,0 +1,108 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:patrol/patrol.dart';
+
+import 'package:personalization_flutter_sdk_example/main.dart' as app;
+
+Future _initializeSdk(PatrolIntegrationTester $) async {
+ await $.pumpWidgetAndSettle(const app.App());
+ await $(
+ 'Status: Initialized',
+ ).waitUntilExists(timeout: const Duration(seconds: 30));
+ await $('Status: Initialized').scrollTo(scrollDirection: AxisDirection.up);
+}
+
+String _labelText(PatrolIntegrationTester $, String key) {
+ final widget = $.tester.widget(find.byKey(Key(key)));
+ return widget.data ?? '';
+}
+
+void main() {
+ // ---------------------------------------------------------------------------
+ // getSid
+ // ---------------------------------------------------------------------------
+ patrolTest('getSid — returns non-empty string after init', ($) async {
+ await _initializeSdk($);
+
+ await $('Get SID').scrollTo();
+ await $('Get SID').tap();
+ await $.pumpAndSettle();
+
+ final sid = _labelText($, 'lbl_sid');
+ expect(sid, isNotEmpty);
+ expect(sid, isNot('—'));
+ expect(sid, isNot(contains('Error')));
+ });
+
+ patrolTest('getSid — consistent value on repeated calls', ($) async {
+ await _initializeSdk($);
+
+ await $('Get SID').scrollTo();
+ await $('Get SID').tap();
+ await $.pumpAndSettle();
+ final first = _labelText($, 'lbl_sid');
+
+ await $('Get SID').scrollTo();
+ await $('Get SID').tap();
+ await $.pumpAndSettle();
+ final second = _labelText($, 'lbl_sid');
+
+ expect(first, equals(second));
+ });
+
+ // ---------------------------------------------------------------------------
+ // getDid
+ // ---------------------------------------------------------------------------
+ patrolTest('getDid — does not crash after init', ($) async {
+ await _initializeSdk($);
+
+ await $('Get DID').scrollTo();
+ await $('Get DID').tap();
+ await $.pumpAndSettle();
+
+ final did = _labelText($, 'lbl_did');
+ expect(did, isNot(contains('Error')));
+ });
+
+ patrolTest('getDid — result is a DID string or null before first sync', (
+ $,
+ ) async {
+ await _initializeSdk($);
+
+ await $('Get DID').scrollTo();
+ await $('Get DID').tap();
+ await $.pumpAndSettle();
+
+ final did = _labelText($, 'lbl_did');
+ // Either the SDK assigned a DID or returned null before the first API sync.
+ expect(did == 'null' || (did.isNotEmpty && did != '—'), isTrue);
+ });
+
+ // ---------------------------------------------------------------------------
+ // setProfile
+ // ---------------------------------------------------------------------------
+ patrolTest('setProfile — completes without error', ($) async {
+ await _initializeSdk($);
+
+ await $('Set Profile').scrollTo();
+ await $('Set Profile').tap();
+ await $.pumpAndSettle();
+
+ final status = _labelText($, 'lbl_profile_status');
+ expect(status, equals('Profile set'));
+ });
+
+ patrolTest('setProfile — can be called multiple times', ($) async {
+ await _initializeSdk($);
+
+ await $('Set Profile').scrollTo();
+ await $('Set Profile').tap();
+ await $.pumpAndSettle();
+ expect(_labelText($, 'lbl_profile_status'), equals('Profile set'));
+
+ await $('Set Profile').scrollTo();
+ await $('Set Profile').tap();
+ await $.pumpAndSettle();
+ expect(_labelText($, 'lbl_profile_status'), equals('Profile set'));
+ });
+}
diff --git a/example/integration_test/push_token_test.dart b/example/integration_test/push_token_test.dart
new file mode 100644
index 0000000..1a762c4
--- /dev/null
+++ b/example/integration_test/push_token_test.dart
@@ -0,0 +1,32 @@
+import 'package:patrol/patrol.dart';
+
+import 'package:personalization_flutter_sdk_example/main.dart' as app;
+
+void main() {
+ patrolTest('push token section is visible on launch', ($) async {
+ await $.pumpWidgetAndSettle(const app.App());
+
+ await $('Stored push token').waitUntilVisible();
+ });
+
+ patrolTest('Refresh and Copy controls are visible', ($) async {
+ await $.pumpWidgetAndSettle(const app.App());
+
+ await $('Refresh').waitUntilVisible();
+ await $('Copy').waitUntilVisible();
+ });
+
+ patrolTest('refreshing the token after auto-init does not crash', ($) async {
+ await $.pumpWidgetAndSettle(const app.App());
+
+ await $(
+ 'Status: Initialized',
+ ).waitUntilVisible(timeout: const Duration(seconds: 30));
+
+ await $('Refresh').tap();
+ await $.pumpAndSettle();
+
+ // Section stays present whether or not a token was delivered yet.
+ await $('Stored push token').waitUntilVisible();
+ });
+}
diff --git a/example/integration_test/recommendation_sdk_test.dart b/example/integration_test/recommendation_sdk_test.dart
new file mode 100644
index 0000000..e2857df
--- /dev/null
+++ b/example/integration_test/recommendation_sdk_test.dart
@@ -0,0 +1,103 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:patrol/patrol.dart';
+
+import 'package:personalization_flutter_sdk_example/main.dart' as app;
+
+import 'test_config.dart';
+
+Future _initializeSdk(PatrolIntegrationTester $) async {
+ await $.pumpWidgetAndSettle(const app.App());
+ await $(
+ 'Status: Initialized',
+ ).waitUntilExists(timeout: const Duration(seconds: 30));
+ await $('Status: Initialized').scrollTo(scrollDirection: AxisDirection.up);
+}
+
+String _labelText(PatrolIntegrationTester $, String key) {
+ final widget = $.tester.widget(find.byKey(Key(key)));
+ return widget.data ?? '';
+}
+
+void main() {
+ // ---------------------------------------------------------------------------
+ // getRecommendation
+ // ---------------------------------------------------------------------------
+ patrolTest('getRecommendation — returns title and product list', ($) async {
+ await _initializeSdk($);
+
+ await $.tester.enterText(
+ find.byKey(const Key('field_rec_block_code')),
+ TestConfig.recommendationBlockCode,
+ );
+ await $.tester.pump();
+
+ await $('Get Recommendations').scrollTo();
+ await $('Get Recommendations').tap();
+
+ // Wait for loading to finish — button text reverts to 'Get Recommendations'.
+ await $('Get Recommendations').waitUntilVisible();
+
+ expect(find.byKey(const Key('lbl_rec_error')), findsNothing);
+
+ final title = _labelText($, 'lbl_rec_title');
+ expect(title, isNot(contains('Error')));
+ expect(title, startsWith('Title:'));
+ });
+
+ patrolTest('getRecommendation — product count is non-negative', ($) async {
+ await _initializeSdk($);
+
+ await $.tester.enterText(
+ find.byKey(const Key('field_rec_block_code')),
+ TestConfig.recommendationBlockCode,
+ );
+ await $.tester.pump();
+ await $('Get Recommendations').scrollTo();
+ await $('Get Recommendations').tap();
+ await $('Get Recommendations').waitUntilVisible();
+
+ final countText = _labelText($, 'lbl_rec_count');
+ // Text is "Products: N" — extract the number.
+ final n = int.tryParse(countText.replaceFirst('Products: ', ''));
+ expect(n, isNotNull);
+ expect(n, greaterThanOrEqualTo(0));
+ });
+
+ patrolTest('getRecommendation — empty block code does not crash', ($) async {
+ await _initializeSdk($);
+
+ // Leave block code field empty and tap.
+ await $('Get Recommendations').scrollTo();
+ await $('Get Recommendations').tap();
+ await $.pumpAndSettle();
+
+ // No error label should appear — empty code is a no-op in the UI.
+ expect(find.byKey(const Key('lbl_rec_title')), findsNothing);
+ expect(find.byKey(const Key('lbl_rec_error')), findsNothing);
+ });
+
+ patrolTest('getRecommendation — invalid block code shows error', ($) async {
+ await _initializeSdk($);
+
+ await $.tester.enterText(
+ find.byKey(const Key('field_rec_block_code')),
+ 'nonexistent_block_xyz',
+ );
+ await $.tester.pump();
+ await $('Get Recommendations').scrollTo();
+ await $('Get Recommendations').tap();
+ await $('Get Recommendations').waitUntilVisible();
+
+ // Either an error label appears, or we get an empty product list — both are valid.
+ final hasError = find
+ .byKey(const Key('lbl_rec_error'))
+ .evaluate()
+ .isNotEmpty;
+ final hasTitle = find
+ .byKey(const Key('lbl_rec_title'))
+ .evaluate()
+ .isNotEmpty;
+ expect(hasError || hasTitle, isTrue);
+ });
+}
diff --git a/example/integration_test/search_blank_sdk_test.dart b/example/integration_test/search_blank_sdk_test.dart
new file mode 100644
index 0000000..845b599
--- /dev/null
+++ b/example/integration_test/search_blank_sdk_test.dart
@@ -0,0 +1,70 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:patrol/patrol.dart';
+
+import 'package:personalization_flutter_sdk_example/main.dart' as app;
+
+Future _initializeSdk(PatrolIntegrationTester $) async {
+ await $.pumpWidgetAndSettle(const app.App());
+ await $(
+ 'Status: Initialized',
+ ).waitUntilExists(timeout: const Duration(seconds: 30));
+ await $('Status: Initialized').scrollTo(scrollDirection: AxisDirection.up);
+}
+
+String _labelText(PatrolIntegrationTester $, String key) {
+ final widget = $.tester.widget(find.byKey(Key(key)));
+ return widget.data ?? '';
+}
+
+void main() {
+ patrolTest('searchBlank — returns products and suggests counts', ($) async {
+ await _initializeSdk($);
+
+ await $('Search Blank').scrollTo();
+ await $('Search Blank').tap();
+ await $('Search Blank').waitUntilVisible();
+
+ expect(find.byKey(const Key('lbl_search_blank_error')), findsNothing);
+
+ final productsText = _labelText($, 'lbl_search_blank_products');
+ final suggestsText = _labelText($, 'lbl_search_blank_suggests');
+
+ expect(productsText, startsWith('Products:'));
+ expect(suggestsText, startsWith('Suggests:'));
+
+ final products = int.tryParse(productsText.replaceFirst('Products: ', ''));
+ final suggests = int.tryParse(suggestsText.replaceFirst('Suggests: ', ''));
+ expect(products, isNotNull);
+ expect(suggests, isNotNull);
+ expect(products, greaterThanOrEqualTo(0));
+ expect(suggests, greaterThanOrEqualTo(0));
+ });
+
+ patrolTest('searchBlank — can be called multiple times', ($) async {
+ await _initializeSdk($);
+
+ await $('Search Blank').scrollTo();
+ await $('Search Blank').tap();
+ await $('Search Blank').waitUntilVisible();
+ final firstProducts = _labelText($, 'lbl_search_blank_products');
+
+ await $('Search Blank').scrollTo();
+ await $('Search Blank').tap();
+ await $('Search Blank').waitUntilVisible();
+ final secondProducts = _labelText($, 'lbl_search_blank_products');
+
+ expect(firstProducts, equals(secondProducts));
+ });
+
+ patrolTest('searchBlank — no error on first call after init', ($) async {
+ await _initializeSdk($);
+
+ await $('Search Blank').scrollTo();
+ await $('Search Blank').tap();
+ await $('Search Blank').waitUntilVisible();
+
+ expect(find.byKey(const Key('lbl_search_blank_error')), findsNothing);
+ expect(find.byKey(const Key('lbl_search_blank_products')), findsOneWidget);
+ });
+}
diff --git a/example/integration_test/search_instant_sdk_test.dart b/example/integration_test/search_instant_sdk_test.dart
new file mode 100644
index 0000000..8218f9b
--- /dev/null
+++ b/example/integration_test/search_instant_sdk_test.dart
@@ -0,0 +1,98 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:patrol/patrol.dart';
+
+import 'package:personalization_flutter_sdk_example/main.dart' as app;
+
+import 'test_config.dart';
+
+Future _initializeSdk(PatrolIntegrationTester $) async {
+ await $.pumpWidgetAndSettle(const app.App());
+ await $(
+ 'Status: Initialized',
+ ).waitUntilExists(timeout: const Duration(seconds: 30));
+ await $('Status: Initialized').scrollTo(scrollDirection: AxisDirection.up);
+}
+
+String _labelText(PatrolIntegrationTester $, String key) {
+ final widget = $.tester.widget(find.byKey(Key(key)));
+ return widget.data ?? '';
+}
+
+void main() {
+ patrolTest('searchInstant — returns products_total for valid query', (
+ $,
+ ) async {
+ await _initializeSdk($);
+
+ await $.tester.enterText(
+ find.byKey(const Key('field_search_instant_query')),
+ TestConfig.searchQuery,
+ );
+ await $.tester.pump();
+
+ await $('Search Instant').tap();
+ await $('Search Instant').waitUntilVisible();
+
+ expect(find.byKey(const Key('lbl_search_instant_error')), findsNothing);
+
+ final totalText = _labelText($, 'lbl_search_instant_total');
+ expect(totalText, startsWith('Total:'));
+ final n = int.tryParse(totalText.replaceFirst('Total: ', ''));
+ expect(n, isNotNull);
+ expect(n, greaterThanOrEqualTo(0));
+ });
+
+ patrolTest('searchInstant — empty query is no-op in the UI', ($) async {
+ await _initializeSdk($);
+
+ await $('Search Instant').tap();
+ await $.pumpAndSettle();
+
+ expect(find.byKey(const Key('lbl_search_instant_total')), findsNothing);
+ expect(find.byKey(const Key('lbl_search_instant_error')), findsNothing);
+ });
+
+ patrolTest('searchInstant — consistent total on repeated calls', ($) async {
+ await _initializeSdk($);
+
+ await $.tester.enterText(
+ find.byKey(const Key('field_search_instant_query')),
+ TestConfig.searchQuery,
+ );
+ await $.tester.pump();
+
+ await $('Search Instant').tap();
+ await $('Search Instant').waitUntilVisible();
+ final first = _labelText($, 'lbl_search_instant_total');
+
+ await $('Search Instant').tap();
+ await $('Search Instant').waitUntilVisible();
+ final second = _labelText($, 'lbl_search_instant_total');
+
+ expect(first, equals(second));
+ });
+
+ patrolTest('searchInstant — unknown query does not crash', ($) async {
+ await _initializeSdk($);
+
+ await $.tester.enterText(
+ find.byKey(const Key('field_search_instant_query')),
+ 'xyzunknown99887766',
+ );
+ await $.tester.pump();
+
+ await $('Search Instant').tap();
+ await $('Search Instant').waitUntilVisible();
+
+ final hasTotal = find
+ .byKey(const Key('lbl_search_instant_total'))
+ .evaluate()
+ .isNotEmpty;
+ final hasError = find
+ .byKey(const Key('lbl_search_instant_error'))
+ .evaluate()
+ .isNotEmpty;
+ expect(hasTotal || hasError, isTrue);
+ });
+}
diff --git a/example/integration_test/search_sdk_test.dart b/example/integration_test/search_sdk_test.dart
new file mode 100644
index 0000000..4c60870
--- /dev/null
+++ b/example/integration_test/search_sdk_test.dart
@@ -0,0 +1,103 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:patrol/patrol.dart';
+
+import 'package:personalization_flutter_sdk_example/main.dart' as app;
+
+import 'test_config.dart';
+
+Future _initializeSdk(PatrolIntegrationTester $) async {
+ await $.pumpWidgetAndSettle(const app.App());
+ await $(
+ 'Status: Initialized',
+ ).waitUntilExists(timeout: const Duration(seconds: 30));
+ await $('Status: Initialized').scrollTo(scrollDirection: AxisDirection.up);
+}
+
+String _labelText(PatrolIntegrationTester $, String key) {
+ final widget = $.tester.widget(find.byKey(Key(key)));
+ return widget.data ?? '';
+}
+
+void main() {
+ patrolTest('searchFull — returns products_total for valid query', ($) async {
+ await _initializeSdk($);
+
+ await $.tester.enterText(
+ find.byKey(const Key('field_search_query')),
+ TestConfig.searchQuery,
+ );
+ await $.tester.pump();
+
+ await $('Search').scrollTo();
+ await $('Search').tap();
+ await $('Search').waitUntilVisible();
+
+ expect(find.byKey(const Key('lbl_search_error')), findsNothing);
+
+ final totalText = _labelText($, 'lbl_search_total');
+ expect(totalText, startsWith('Total:'));
+ final n = int.tryParse(totalText.replaceFirst('Total: ', ''));
+ expect(n, isNotNull);
+ expect(n, greaterThanOrEqualTo(0));
+ });
+
+ patrolTest('searchFull — empty query is no-op in the UI', ($) async {
+ await _initializeSdk($);
+
+ await $('Search').scrollTo();
+ await $('Search').tap();
+ await $.pumpAndSettle();
+
+ expect(find.byKey(const Key('lbl_search_total')), findsNothing);
+ expect(find.byKey(const Key('lbl_search_error')), findsNothing);
+ });
+
+ patrolTest('searchFull — can be called multiple times', ($) async {
+ await _initializeSdk($);
+
+ await $.tester.enterText(
+ find.byKey(const Key('field_search_query')),
+ TestConfig.searchQuery,
+ );
+ await $.tester.pump();
+
+ await $('Search').scrollTo();
+ await $('Search').tap();
+ await $('Search').waitUntilVisible();
+ final first = _labelText($, 'lbl_search_total');
+
+ await $('Search').scrollTo();
+ await $('Search').tap();
+ await $('Search').waitUntilVisible();
+ final second = _labelText($, 'lbl_search_total');
+
+ expect(first, equals(second));
+ });
+
+ patrolTest('searchFull — unknown query returns result or error, no crash', (
+ $,
+ ) async {
+ await _initializeSdk($);
+
+ await $.tester.enterText(
+ find.byKey(const Key('field_search_query')),
+ 'xyznotexistentproduct12345',
+ );
+ await $.tester.pump();
+
+ await $('Search').scrollTo();
+ await $('Search').tap();
+ await $('Search').waitUntilVisible();
+
+ final hasTotal = find
+ .byKey(const Key('lbl_search_total'))
+ .evaluate()
+ .isNotEmpty;
+ final hasError = find
+ .byKey(const Key('lbl_search_error'))
+ .evaluate()
+ .isNotEmpty;
+ expect(hasTotal || hasError, isTrue);
+ });
+}
diff --git a/example/integration_test/test_bundle.dart b/example/integration_test/test_bundle.dart
new file mode 100644
index 0000000..4cc56c4
--- /dev/null
+++ b/example/integration_test/test_bundle.dart
@@ -0,0 +1,89 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND AND DO NOT COMMIT TO VERSION CONTROL
+// ignore_for_file: type=lint, invalid_use_of_internal_member
+
+import 'dart:async';
+
+import 'package:flutter_test/flutter_test.dart';
+import 'package:patrol/patrol.dart';
+import 'package:patrol/src/platform/contracts/contracts.dart';
+import 'package:test_api/src/backend/invoker.dart';
+
+// START: GENERATED TEST IMPORTS
+import 'search_blank_sdk_test.dart' as search_blank_sdk_test;
+// END: GENERATED TEST IMPORTS
+
+Future main() async {
+ // This is the entrypoint of the bundled Dart test.
+ //
+ // Its responsibilities are:
+ // * Running a special Dart test that runs before all the other tests and
+ // explores the hierarchy of groups and tests.
+ // * Hosting a PatrolAppService, which the native side of Patrol uses to get
+ // the Dart tests, and to request execution of a specific Dart test.
+ //
+ // When running on Android, the Android Test Orchestrator, before running the
+ // tests, makes an initial run to gather the tests that it will later run. The
+ // native side of Patrol (specifically: PatrolJUnitRunner class) is hooked
+ // into the Android Test Orchestrator lifecycle and knows when that initial
+ // run happens. When it does, PatrolJUnitRunner makes an RPC call to
+ // PatrolAppService and asks it for Dart tests.
+ //
+ // When running on iOS, the native side of Patrol (specifically: the
+ // PATROL_INTEGRATION_TEST_IOS_RUNNER macro) makes an initial run to gather
+ // the tests that it will later run (same as the Android). During that initial
+ // run, it makes an RPC call to PatrolAppService and asks it for Dart tests.
+ //
+ // Once the native runner has the list of Dart tests, it dynamically creates
+ // native test cases from them. On Android, this is done using the
+ // Parametrized JUnit runner. On iOS, new test case methods are swizzled into
+ // the RunnerUITests class, taking advantage of the very dynamic nature of
+ // Objective-C runtime.
+ //
+ // Execution of these dynamically created native test cases is then fully
+ // managed by the underlying native test framework (JUnit on Android, XCTest
+ // on iOS). The native test cases do only one thing - request execution of the
+ // Dart test (out of which they had been created) and wait for it to complete.
+ // The result of running the Dart test is the result of the native test case.
+
+ final platformAutomator = PlatformAutomator(
+ config: PlatformAutomatorConfig.defaultConfig(),
+ );
+ await platformAutomator.initialize();
+ final binding = PatrolBinding.ensureInitialized(platformAutomator);
+ final testExplorationCompleter = Completer();
+
+ // A special test to explore the hierarchy of groups and tests. This is a hack
+ // around https://github.com/dart-lang/test/issues/1998.
+ //
+ // This test must be the first to run. If not, the native side likely won't
+ // receive any tests, and everything will fall apart.
+ test('patrol_test_explorer', () {
+ // Maybe somewhat counterintuitively, this callback runs *after* the calls
+ // to group() below.
+ final topLevelGroup = Invoker.current!.liveTest.groups.first;
+ final dartTestGroup = createDartTestGroup(
+ topLevelGroup,
+ tags: null,
+ excludeTags: null,
+ );
+ testExplorationCompleter.complete(dartTestGroup);
+ print('patrol_test_explorer: obtained Dart-side test hierarchy:');
+ reportGroupStructure(dartTestGroup);
+ });
+
+ // START: GENERATED TEST GROUPS
+ group('search_blank_sdk_test', search_blank_sdk_test.main);
+ // END: GENERATED TEST GROUPS
+
+ final dartTestGroup = await testExplorationCompleter.future;
+ final appService = PatrolAppService(topLevelDartTestGroup: dartTestGroup);
+ binding.patrolAppService = appService;
+ await runAppService(appService);
+
+ // Until now, the native test runner was waiting for us, the Dart side, to
+ // come alive. Now that we did, let's tell it that we're ready to be asked
+ // about Dart tests.
+ await platformAutomator.markPatrolAppServiceReady();
+
+ await appService.testExecutionCompleted;
+}
diff --git a/example/integration_test/test_config.dart b/example/integration_test/test_config.dart
new file mode 100644
index 0000000..78f06bb
--- /dev/null
+++ b/example/integration_test/test_config.dart
@@ -0,0 +1,19 @@
+/// Test credentials for integration tests.
+///
+/// shopId / productId / searchQuery taken from native SDK test suites.
+/// recommendationBlockCode must be filled in manually (not stored in source).
+/// Do NOT commit real production credentials to version control.
+class TestConfig {
+ static const shopId = '357382bf66ac0ce2f1722677c59511';
+ static const apiDomain = 'api.rees46.ru';
+
+ /// A recommender block code that exists in your test shop.
+ /// Not found in any native SDK source — fill in from the REES46 dashboard.
+ static const recommendationBlockCode = 'your_block_code';
+
+ /// A search query that returns at least one result in your test shop.
+ static const searchQuery = 'пудра-бронзер';
+
+ /// A product ID that exists in your test shop.
+ static const productId = '486';
+}
diff --git a/example/ios/.gitignore b/example/ios/.gitignore
new file mode 100644
index 0000000..7a7f987
--- /dev/null
+++ b/example/ios/.gitignore
@@ -0,0 +1,34 @@
+**/dgph
+*.mode1v3
+*.mode2v3
+*.moved-aside
+*.pbxuser
+*.perspectivev3
+**/*sync/
+.sconsign.dblite
+.tags*
+**/.vagrant/
+**/DerivedData/
+Icon?
+**/Pods/
+**/.symlinks/
+profile
+xcuserdata
+**/.generated/
+Flutter/App.framework
+Flutter/Flutter.framework
+Flutter/Flutter.podspec
+Flutter/Generated.xcconfig
+Flutter/ephemeral/
+Flutter/app.flx
+Flutter/app.zip
+Flutter/flutter_assets/
+Flutter/flutter_export_environment.sh
+ServiceDefinitions.json
+Runner/GeneratedPluginRegistrant.*
+
+# Exceptions to above rules.
+!default.mode1v3
+!default.mode2v3
+!default.pbxuser
+!default.perspectivev3
diff --git a/example/ios/Flutter/AppFrameworkInfo.plist b/example/ios/Flutter/AppFrameworkInfo.plist
new file mode 100644
index 0000000..391a902
--- /dev/null
+++ b/example/ios/Flutter/AppFrameworkInfo.plist
@@ -0,0 +1,24 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ en
+ CFBundleExecutable
+ App
+ CFBundleIdentifier
+ io.flutter.flutter.app
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ App
+ CFBundlePackageType
+ FMWK
+ CFBundleShortVersionString
+ 1.0
+ CFBundleSignature
+ ????
+ CFBundleVersion
+ 1.0
+
+
diff --git a/example/ios/Flutter/Debug.xcconfig b/example/ios/Flutter/Debug.xcconfig
new file mode 100644
index 0000000..ec97fc6
--- /dev/null
+++ b/example/ios/Flutter/Debug.xcconfig
@@ -0,0 +1,2 @@
+#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
+#include "Generated.xcconfig"
diff --git a/example/ios/Flutter/Release.xcconfig b/example/ios/Flutter/Release.xcconfig
new file mode 100644
index 0000000..c4855bf
--- /dev/null
+++ b/example/ios/Flutter/Release.xcconfig
@@ -0,0 +1,2 @@
+#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
+#include "Generated.xcconfig"
diff --git a/example/ios/Podfile b/example/ios/Podfile
new file mode 100644
index 0000000..5612959
--- /dev/null
+++ b/example/ios/Podfile
@@ -0,0 +1,47 @@
+# Uncomment this line to define a global platform for your project
+# platform :ios, '13.0'
+
+# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
+ENV['COCOAPODS_DISABLE_STATS'] = 'true'
+
+project 'Runner', {
+ 'Debug' => :debug,
+ 'Profile' => :release,
+ 'Release' => :release,
+}
+
+def flutter_root
+ generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
+ unless File.exist?(generated_xcode_build_settings_path)
+ raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
+ end
+
+ File.foreach(generated_xcode_build_settings_path) do |line|
+ matches = line.match(/FLUTTER_ROOT\=(.*)/)
+ return matches[1].strip if matches
+ end
+ raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
+end
+
+require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
+
+flutter_ios_podfile_setup
+
+target 'Runner' do
+ use_frameworks!
+
+ target 'RunnerUITests' do
+ inherit! :complete
+ end
+
+ flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
+ target 'RunnerTests' do
+ inherit! :search_paths
+ end
+end
+
+post_install do |installer|
+ installer.pods_project.targets.each do |target|
+ flutter_additional_ios_build_settings(target)
+ end
+end
diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock
new file mode 100644
index 0000000..ac25e8f
--- /dev/null
+++ b/example/ios/Podfile.lock
@@ -0,0 +1,46 @@
+PODS:
+ - CocoaAsyncSocket (7.6.5)
+ - Flutter (1.0.0)
+ - integration_test (0.0.1):
+ - Flutter
+ - patrol (0.0.1):
+ - CocoaAsyncSocket (~> 7.6)
+ - Flutter
+ - FlutterMacOS
+ - personalization_flutter_sdk (0.0.1):
+ - Flutter
+ - REES46 (= 3.23.0)
+ - REES46 (3.23.0)
+
+DEPENDENCIES:
+ - Flutter (from `Flutter`)
+ - integration_test (from `.symlinks/plugins/integration_test/ios`)
+ - patrol (from `.symlinks/plugins/patrol/darwin`)
+ - personalization_flutter_sdk (from `.symlinks/plugins/personalization_flutter_sdk/ios`)
+
+SPEC REPOS:
+ trunk:
+ - CocoaAsyncSocket
+ - REES46
+
+EXTERNAL SOURCES:
+ Flutter:
+ :path: Flutter
+ integration_test:
+ :path: ".symlinks/plugins/integration_test/ios"
+ patrol:
+ :path: ".symlinks/plugins/patrol/darwin"
+ personalization_flutter_sdk:
+ :path: ".symlinks/plugins/personalization_flutter_sdk/ios"
+
+SPEC CHECKSUMS:
+ CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99
+ Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
+ integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e
+ patrol: cea8074f183a2a4232d0ebd10569ae05149ada42
+ personalization_flutter_sdk: 296eed2fbff082c9697c96d2176b023c173bb375
+ REES46: 48ad09f18e2d75a730728c7e10f0328ffac69b08
+
+PODFILE CHECKSUM: b6e248ac4c1eff5807c8b044b39b8bc326af3f5f
+
+COCOAPODS: 1.16.2
diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj
new file mode 100644
index 0000000..72e3478
--- /dev/null
+++ b/example/ios/Runner.xcodeproj/project.pbxproj
@@ -0,0 +1,889 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 54;
+ objects = {
+
+/* Begin PBXBuildFile section */
+ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
+ 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
+ 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
+ 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
+ 7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */; };
+ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
+ 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
+ 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
+ A7B83A61A5E2A2634D4B68DD /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 20B4AA99524AAD3578A54D95 /* Pods_Runner.framework */; };
+ AA11BB22CC33DD4400EEFF01 /* RunnerUITests.m in Sources */ = {isa = PBXBuildFile; fileRef = AA11BB22CC33DD4400EEFF00 /* RunnerUITests.m */; };
+ B632BB76B3DE033E33C3AF7C /* Pods_Runner_RunnerUITests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 96DA490D0CB17C99E25655CC /* Pods_Runner_RunnerUITests.framework */; };
+ D1AEE439B64D48FFC231B7FD /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 101FDAA560BF8F521BAB8014 /* Pods_RunnerTests.framework */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXContainerItemProxy section */
+ 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 97C146E61CF9000F007C117D /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = 97C146ED1CF9000F007C117D;
+ remoteInfo = Runner;
+ };
+/* End PBXContainerItemProxy section */
+
+/* Begin PBXCopyFilesBuildPhase section */
+ 9705A1C41CF9048500538489 /* Embed Frameworks */ = {
+ isa = PBXCopyFilesBuildPhase;
+ buildActionMask = 2147483647;
+ dstPath = "";
+ dstSubfolderSpec = 10;
+ files = (
+ );
+ name = "Embed Frameworks";
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXCopyFilesBuildPhase section */
+
+/* Begin PBXFileReference section */
+ 101FDAA560BF8F521BAB8014 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; };
+ 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; };
+ 1DF40DFCE9121725D0BC8456 /* Pods-Runner-RunnerUITests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner-RunnerUITests.profile.xcconfig"; path = "Target Support Files/Pods-Runner-RunnerUITests/Pods-Runner-RunnerUITests.profile.xcconfig"; sourceTree = ""; };
+ 20B4AA99524AAD3578A54D95 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ 25281CD9083CDCD536709C9E /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; };
+ 3059447129043636022194F8 /* Pods-Runner-RunnerUITests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner-RunnerUITests.release.xcconfig"; path = "Target Support Files/Pods-Runner-RunnerUITests/Pods-Runner-RunnerUITests.release.xcconfig"; sourceTree = ""; };
+ 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; };
+ 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
+ 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; };
+ 47A6BF8207A65A857E1F9E2C /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; };
+ 6BF98BB0E22B7F76759C021D /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; };
+ 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; };
+ 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
+ 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; };
+ 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; };
+ 86D4BDD5ECC7DC2C16E64E8C /* Pods-Runner-RunnerUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner-RunnerUITests.debug.xcconfig"; path = "Target Support Files/Pods-Runner-RunnerUITests/Pods-Runner-RunnerUITests.debug.xcconfig"; sourceTree = ""; };
+ 94AEB8B21E6E31B2E195CD5A /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; };
+ 96DA490D0CB17C99E25655CC /* Pods_Runner_RunnerUITests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner_RunnerUITests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; };
+ 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; };
+ 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; };
+ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
+ 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
+ 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
+ AA11BB22CC33DD4400EEFF00 /* RunnerUITests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RunnerUITests.m; sourceTree = ""; };
+ AA11BB22CC33DD4400EEFF04 /* RunnerUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
+ AA11BB22CC33DD4400EEFF05 /* Pods-RunnerUITests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerUITests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerUITests/Pods-RunnerUITests.debug.xcconfig"; sourceTree = ""; };
+ AA11BB22CC33DD4400EEFF06 /* Pods-RunnerUITests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerUITests.release.xcconfig"; path = "Target Support Files/Pods-RunnerUITests/Pods-RunnerUITests.release.xcconfig"; sourceTree = ""; };
+ AA11BB22CC33DD4400EEFF07 /* Pods-RunnerUITests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerUITests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerUITests/Pods-RunnerUITests.profile.xcconfig"; sourceTree = ""; };
+ C8A20CF582848CCD4E47E3D6 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; };
+ CF01641D7F1716455460D103 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ 97C146EB1CF9000F007C117D /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ A7B83A61A5E2A2634D4B68DD /* Pods_Runner.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ AA11BB22CC33DD4400EEFF08 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ B632BB76B3DE033E33C3AF7C /* Pods_Runner_RunnerUITests.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ FF9C9FE2AEF5CB467BB081EB /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ D1AEE439B64D48FFC231B7FD /* Pods_RunnerTests.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ 331C8082294A63A400263BE5 /* RunnerTests */ = {
+ isa = PBXGroup;
+ children = (
+ 331C807B294A618700263BE5 /* RunnerTests.swift */,
+ );
+ path = RunnerTests;
+ sourceTree = "";
+ };
+ 9740EEB11CF90186004384FC /* Flutter */ = {
+ isa = PBXGroup;
+ children = (
+ 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
+ 9740EEB21CF90195004384FC /* Debug.xcconfig */,
+ 7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
+ 9740EEB31CF90195004384FC /* Generated.xcconfig */,
+ );
+ name = Flutter;
+ sourceTree = "";
+ };
+ 97C146E51CF9000F007C117D = {
+ isa = PBXGroup;
+ children = (
+ 9740EEB11CF90186004384FC /* Flutter */,
+ 97C146F01CF9000F007C117D /* Runner */,
+ 97C146EF1CF9000F007C117D /* Products */,
+ 331C8082294A63A400263BE5 /* RunnerTests */,
+ AA11BB22CC33DD4400EEFF09 /* RunnerUITests */,
+ F36E8928AE64637CFEACB6DC /* Pods */,
+ E72F27077C97822DF9AF5048 /* Frameworks */,
+ );
+ sourceTree = "";
+ };
+ 97C146EF1CF9000F007C117D /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 97C146EE1CF9000F007C117D /* Runner.app */,
+ 331C8081294A63A400263BE5 /* RunnerTests.xctest */,
+ AA11BB22CC33DD4400EEFF04 /* RunnerUITests.xctest */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ 97C146F01CF9000F007C117D /* Runner */ = {
+ isa = PBXGroup;
+ children = (
+ 97C146FA1CF9000F007C117D /* Main.storyboard */,
+ 97C146FD1CF9000F007C117D /* Assets.xcassets */,
+ 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
+ 97C147021CF9000F007C117D /* Info.plist */,
+ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
+ 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
+ 74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
+ 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */,
+ 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
+ );
+ path = Runner;
+ sourceTree = "";
+ };
+ AA11BB22CC33DD4400EEFF09 /* RunnerUITests */ = {
+ isa = PBXGroup;
+ children = (
+ AA11BB22CC33DD4400EEFF00 /* RunnerUITests.m */,
+ );
+ path = RunnerUITests;
+ sourceTree = "";
+ };
+ E72F27077C97822DF9AF5048 /* Frameworks */ = {
+ isa = PBXGroup;
+ children = (
+ 20B4AA99524AAD3578A54D95 /* Pods_Runner.framework */,
+ 101FDAA560BF8F521BAB8014 /* Pods_RunnerTests.framework */,
+ 96DA490D0CB17C99E25655CC /* Pods_Runner_RunnerUITests.framework */,
+ );
+ name = Frameworks;
+ sourceTree = "";
+ };
+ F36E8928AE64637CFEACB6DC /* Pods */ = {
+ isa = PBXGroup;
+ children = (
+ 94AEB8B21E6E31B2E195CD5A /* Pods-Runner.debug.xcconfig */,
+ 6BF98BB0E22B7F76759C021D /* Pods-Runner.release.xcconfig */,
+ CF01641D7F1716455460D103 /* Pods-Runner.profile.xcconfig */,
+ 47A6BF8207A65A857E1F9E2C /* Pods-RunnerTests.debug.xcconfig */,
+ 25281CD9083CDCD536709C9E /* Pods-RunnerTests.release.xcconfig */,
+ C8A20CF582848CCD4E47E3D6 /* Pods-RunnerTests.profile.xcconfig */,
+ AA11BB22CC33DD4400EEFF05 /* Pods-RunnerUITests.debug.xcconfig */,
+ AA11BB22CC33DD4400EEFF06 /* Pods-RunnerUITests.release.xcconfig */,
+ AA11BB22CC33DD4400EEFF07 /* Pods-RunnerUITests.profile.xcconfig */,
+ 86D4BDD5ECC7DC2C16E64E8C /* Pods-Runner-RunnerUITests.debug.xcconfig */,
+ 3059447129043636022194F8 /* Pods-Runner-RunnerUITests.release.xcconfig */,
+ 1DF40DFCE9121725D0BC8456 /* Pods-Runner-RunnerUITests.profile.xcconfig */,
+ );
+ name = Pods;
+ path = Pods;
+ sourceTree = "";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ 331C8080294A63A400263BE5 /* RunnerTests */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
+ buildPhases = (
+ B1B2B2F47EA02153B0B5E6BC /* [CP] Check Pods Manifest.lock */,
+ 331C807D294A63A400263BE5 /* Sources */,
+ 331C807F294A63A400263BE5 /* Resources */,
+ FF9C9FE2AEF5CB467BB081EB /* Frameworks */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ 331C8086294A63A400263BE5 /* PBXTargetDependency */,
+ );
+ name = RunnerTests;
+ productName = RunnerTests;
+ productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
+ productType = "com.apple.product-type.bundle.unit-test";
+ };
+ 97C146ED1CF9000F007C117D /* Runner */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
+ buildPhases = (
+ 19A191DE9373D7C8F4407517 /* [CP] Check Pods Manifest.lock */,
+ 9740EEB61CF901F6004384FC /* Run Script */,
+ 97C146EA1CF9000F007C117D /* Sources */,
+ 97C146EB1CF9000F007C117D /* Frameworks */,
+ 97C146EC1CF9000F007C117D /* Resources */,
+ 9705A1C41CF9048500538489 /* Embed Frameworks */,
+ 3B06AD1E1E4923F5004D2608 /* Thin Binary */,
+ A99D90DAE83D5169298658CC /* [CP] Embed Pods Frameworks */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = Runner;
+ productName = Runner;
+ productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
+ productType = "com.apple.product-type.application";
+ };
+ AA11BB22CC33DD4400EEFF0B /* RunnerUITests */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = AA11BB22CC33DD4400EEFF0C /* Build configuration list for PBXNativeTarget "RunnerUITests" */;
+ buildPhases = (
+ 9A77EDE915878325A4720B12 /* [CP] Check Pods Manifest.lock */,
+ AA11BB22CC33DD4400EEFF0A /* Sources */,
+ AA11BB22CC33DD4400EEFF08 /* Frameworks */,
+ 4027F5FAFA76DC5F18F79BD3 /* [CP] Embed Pods Frameworks */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = RunnerUITests;
+ productName = RunnerUITests;
+ productReference = AA11BB22CC33DD4400EEFF04 /* RunnerUITests.xctest */;
+ productType = "com.apple.product-type.bundle.ui-testing";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ 97C146E61CF9000F007C117D /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ BuildIndependentTargetsInParallel = YES;
+ LastUpgradeCheck = 1510;
+ ORGANIZATIONNAME = "";
+ TargetAttributes = {
+ 331C8080294A63A400263BE5 = {
+ CreatedOnToolsVersion = 14.0;
+ TestTargetID = 97C146ED1CF9000F007C117D;
+ };
+ 97C146ED1CF9000F007C117D = {
+ CreatedOnToolsVersion = 7.3.1;
+ LastSwiftMigration = 1100;
+ };
+ };
+ };
+ buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
+ compatibilityVersion = "Xcode 9.3";
+ developmentRegion = en;
+ hasScannedForEncodings = 0;
+ knownRegions = (
+ en,
+ Base,
+ );
+ mainGroup = 97C146E51CF9000F007C117D;
+ productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
+ projectDirPath = "";
+ projectRoot = "";
+ targets = (
+ 97C146ED1CF9000F007C117D /* Runner */,
+ 331C8080294A63A400263BE5 /* RunnerTests */,
+ AA11BB22CC33DD4400EEFF0B /* RunnerUITests */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+ 331C807F294A63A400263BE5 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 97C146EC1CF9000F007C117D /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
+ 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
+ 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
+ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXShellScriptBuildPhase section */
+ 19A191DE9373D7C8F4407517 /* [CP] Check Pods Manifest.lock */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ );
+ inputPaths = (
+ "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
+ "${PODS_ROOT}/Manifest.lock",
+ );
+ name = "[CP] Check Pods Manifest.lock";
+ outputFileListPaths = (
+ );
+ outputPaths = (
+ "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
+ showEnvVarsInLog = 0;
+ };
+ 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
+ isa = PBXShellScriptBuildPhase;
+ alwaysOutOfDate = 1;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
+ );
+ name = "Thin Binary";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
+ };
+ 4027F5FAFA76DC5F18F79BD3 /* [CP] Embed Pods Frameworks */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-Runner-RunnerUITests/Pods-Runner-RunnerUITests-frameworks-${CONFIGURATION}-input-files.xcfilelist",
+ );
+ name = "[CP] Embed Pods Frameworks";
+ outputFileListPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-Runner-RunnerUITests/Pods-Runner-RunnerUITests-frameworks-${CONFIGURATION}-output-files.xcfilelist",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner-RunnerUITests/Pods-Runner-RunnerUITests-frameworks.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
+ 9740EEB61CF901F6004384FC /* Run Script */ = {
+ isa = PBXShellScriptBuildPhase;
+ alwaysOutOfDate = 1;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "Run Script";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
+ };
+ 9A77EDE915878325A4720B12 /* [CP] Check Pods Manifest.lock */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ );
+ inputPaths = (
+ "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
+ "${PODS_ROOT}/Manifest.lock",
+ );
+ name = "[CP] Check Pods Manifest.lock";
+ outputFileListPaths = (
+ );
+ outputPaths = (
+ "$(DERIVED_FILE_DIR)/Pods-Runner-RunnerUITests-checkManifestLockResult.txt",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
+ showEnvVarsInLog = 0;
+ };
+ A99D90DAE83D5169298658CC /* [CP] Embed Pods Frameworks */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
+ );
+ name = "[CP] Embed Pods Frameworks";
+ outputFileListPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
+ B1B2B2F47EA02153B0B5E6BC /* [CP] Check Pods Manifest.lock */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ );
+ inputPaths = (
+ "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
+ "${PODS_ROOT}/Manifest.lock",
+ );
+ name = "[CP] Check Pods Manifest.lock";
+ outputFileListPaths = (
+ );
+ outputPaths = (
+ "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
+ showEnvVarsInLog = 0;
+ };
+/* End PBXShellScriptBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+ 331C807D294A63A400263BE5 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 97C146EA1CF9000F007C117D /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
+ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
+ 7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ AA11BB22CC33DD4400EEFF0A /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ AA11BB22CC33DD4400EEFF01 /* RunnerUITests.m in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXTargetDependency section */
+ 331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = 97C146ED1CF9000F007C117D /* Runner */;
+ targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
+ };
+/* End PBXTargetDependency section */
+
+/* Begin PBXVariantGroup section */
+ 97C146FA1CF9000F007C117D /* Main.storyboard */ = {
+ isa = PBXVariantGroup;
+ children = (
+ 97C146FB1CF9000F007C117D /* Base */,
+ );
+ name = Main.storyboard;
+ sourceTree = "";
+ };
+ 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
+ isa = PBXVariantGroup;
+ children = (
+ 97C147001CF9000F007C117D /* Base */,
+ );
+ name = LaunchScreen.storyboard;
+ sourceTree = "";
+ };
+/* End PBXVariantGroup section */
+
+/* Begin XCBuildConfiguration section */
+ 249021D3217E4FDB00AE95B9 /* Profile */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_USER_SCRIPT_SANDBOXING = NO;
+ GCC_C_LANGUAGE_STANDARD = gnu99;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 13.0;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ SDKROOT = iphoneos;
+ SUPPORTED_PLATFORMS = iphoneos;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ VALIDATE_PRODUCT = YES;
+ };
+ name = Profile;
+ };
+ 249021D4217E4FDB00AE95B9 /* Profile */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ENABLE_MODULES = YES;
+ CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
+ DEVELOPMENT_TEAM = 2CCD47244M;
+ ENABLE_BITCODE = NO;
+ INFOPLIST_FILE = Runner/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = com.rees46.rees46FlutterSdkExample;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
+ SWIFT_VERSION = 5.0;
+ VERSIONING_SYSTEM = "apple-generic";
+ };
+ name = Profile;
+ };
+ 331C8088294A63A400263BE5 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 47A6BF8207A65A857E1F9E2C /* Pods-RunnerTests.debug.xcconfig */;
+ buildSettings = {
+ BUNDLE_LOADER = "$(TEST_HOST)";
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ GENERATE_INFOPLIST_FILE = YES;
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = com.rees46.rees46FlutterSdkExample.RunnerTests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ SWIFT_VERSION = 5.0;
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
+ };
+ name = Debug;
+ };
+ 331C8089294A63A400263BE5 /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 25281CD9083CDCD536709C9E /* Pods-RunnerTests.release.xcconfig */;
+ buildSettings = {
+ BUNDLE_LOADER = "$(TEST_HOST)";
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ GENERATE_INFOPLIST_FILE = YES;
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = com.rees46.rees46FlutterSdkExample.RunnerTests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_VERSION = 5.0;
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
+ };
+ name = Release;
+ };
+ 331C808A294A63A400263BE5 /* Profile */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = C8A20CF582848CCD4E47E3D6 /* Pods-RunnerTests.profile.xcconfig */;
+ buildSettings = {
+ BUNDLE_LOADER = "$(TEST_HOST)";
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ GENERATE_INFOPLIST_FILE = YES;
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = com.rees46.rees46FlutterSdkExample.RunnerTests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_VERSION = 5.0;
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
+ };
+ name = Profile;
+ };
+ 97C147031CF9000F007C117D /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = dwarf;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_TESTABILITY = YES;
+ ENABLE_USER_SCRIPT_SANDBOXING = NO;
+ GCC_C_LANGUAGE_STANDARD = gnu99;
+ GCC_DYNAMIC_NO_PIC = NO;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_OPTIMIZATION_LEVEL = 0;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "DEBUG=1",
+ "$(inherited)",
+ );
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 13.0;
+ MTL_ENABLE_DEBUG_INFO = YES;
+ ONLY_ACTIVE_ARCH = YES;
+ SDKROOT = iphoneos;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Debug;
+ };
+ 97C147041CF9000F007C117D /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_USER_SCRIPT_SANDBOXING = NO;
+ GCC_C_LANGUAGE_STANDARD = gnu99;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 13.0;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ SDKROOT = iphoneos;
+ SUPPORTED_PLATFORMS = iphoneos;
+ SWIFT_COMPILATION_MODE = wholemodule;
+ SWIFT_OPTIMIZATION_LEVEL = "-O";
+ TARGETED_DEVICE_FAMILY = "1,2";
+ VALIDATE_PRODUCT = YES;
+ };
+ name = Release;
+ };
+ 97C147061CF9000F007C117D /* Debug */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ENABLE_MODULES = YES;
+ CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
+ DEVELOPMENT_TEAM = 2CCD47244M;
+ ENABLE_BITCODE = NO;
+ INFOPLIST_FILE = Runner/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = com.rees46.rees46FlutterSdkExample;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ SWIFT_VERSION = 5.0;
+ VERSIONING_SYSTEM = "apple-generic";
+ };
+ name = Debug;
+ };
+ 97C147071CF9000F007C117D /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ENABLE_MODULES = YES;
+ CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
+ DEVELOPMENT_TEAM = 2CCD47244M;
+ ENABLE_BITCODE = NO;
+ INFOPLIST_FILE = Runner/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = com.rees46.rees46FlutterSdkExample;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
+ SWIFT_VERSION = 5.0;
+ VERSIONING_SYSTEM = "apple-generic";
+ };
+ name = Release;
+ };
+ AA11BB22CC33DD4400EEFF10 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 86D4BDD5ECC7DC2C16E64E8C /* Pods-Runner-RunnerUITests.debug.xcconfig */;
+ buildSettings = {
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ GENERATE_INFOPLIST_FILE = YES;
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = com.rees46.rees46FlutterSdkExample.RunnerUITests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ TEST_TARGET_NAME = Runner;
+ };
+ name = Debug;
+ };
+ AA11BB22CC33DD4400EEFF11 /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 3059447129043636022194F8 /* Pods-Runner-RunnerUITests.release.xcconfig */;
+ buildSettings = {
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ GENERATE_INFOPLIST_FILE = YES;
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = com.rees46.rees46FlutterSdkExample.RunnerUITests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ TEST_TARGET_NAME = Runner;
+ };
+ name = Release;
+ };
+ AA11BB22CC33DD4400EEFF12 /* Profile */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 1DF40DFCE9121725D0BC8456 /* Pods-Runner-RunnerUITests.profile.xcconfig */;
+ buildSettings = {
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ GENERATE_INFOPLIST_FILE = YES;
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = com.rees46.rees46FlutterSdkExample.RunnerUITests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ TEST_TARGET_NAME = Runner;
+ };
+ name = Profile;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 331C8088294A63A400263BE5 /* Debug */,
+ 331C8089294A63A400263BE5 /* Release */,
+ 331C808A294A63A400263BE5 /* Profile */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 97C147031CF9000F007C117D /* Debug */,
+ 97C147041CF9000F007C117D /* Release */,
+ 249021D3217E4FDB00AE95B9 /* Profile */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 97C147061CF9000F007C117D /* Debug */,
+ 97C147071CF9000F007C117D /* Release */,
+ 249021D4217E4FDB00AE95B9 /* Profile */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ AA11BB22CC33DD4400EEFF0C /* Build configuration list for PBXNativeTarget "RunnerUITests" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ AA11BB22CC33DD4400EEFF10 /* Debug */,
+ AA11BB22CC33DD4400EEFF11 /* Release */,
+ AA11BB22CC33DD4400EEFF12 /* Profile */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+/* End XCConfigurationList section */
+ };
+ rootObject = 97C146E61CF9000F007C117D /* Project object */;
+}
diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..919434a
--- /dev/null
+++ b/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 0000000..18d9810
--- /dev/null
+++ b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
new file mode 100644
index 0000000..f9b0d7c
--- /dev/null
+++ b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
@@ -0,0 +1,8 @@
+
+
+
+
+ PreviewsEnabled
+
+
+
diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
new file mode 100644
index 0000000..4df9df3
--- /dev/null
+++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
@@ -0,0 +1,111 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/example/ios/Runner.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..21a3cc1
--- /dev/null
+++ b/example/ios/Runner.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
diff --git a/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 0000000..18d9810
--- /dev/null
+++ b/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
new file mode 100644
index 0000000..f9b0d7c
--- /dev/null
+++ b/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
@@ -0,0 +1,8 @@
+
+
+
+
+ PreviewsEnabled
+
+
+
diff --git a/example/ios/Runner/AppDelegate.swift b/example/ios/Runner/AppDelegate.swift
new file mode 100644
index 0000000..c30b367
--- /dev/null
+++ b/example/ios/Runner/AppDelegate.swift
@@ -0,0 +1,16 @@
+import Flutter
+import UIKit
+
+@main
+@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
+ override func application(
+ _ application: UIApplication,
+ didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
+ ) -> Bool {
+ return super.application(application, didFinishLaunchingWithOptions: launchOptions)
+ }
+
+ func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
+ GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
+ }
+}
diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 0000000..d36b1fa
--- /dev/null
+++ b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,122 @@
+{
+ "images" : [
+ {
+ "size" : "20x20",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-20x20@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "20x20",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-20x20@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-29x29@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-29x29@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-29x29@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-40x40@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-40x40@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "60x60",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-60x60@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "60x60",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-60x60@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "20x20",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-20x20@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "20x20",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-20x20@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-29x29@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-29x29@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-40x40@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-40x40@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "76x76",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-76x76@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "76x76",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-76x76@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "83.5x83.5",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-83.5x83.5@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "1024x1024",
+ "idiom" : "ios-marketing",
+ "filename" : "Icon-App-1024x1024@1x.png",
+ "scale" : "1x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
new file mode 100644
index 0000000..dc9ada4
Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ
diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
new file mode 100644
index 0000000..7353c41
Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ
diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
new file mode 100644
index 0000000..797d452
Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ
diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
new file mode 100644
index 0000000..6ed2d93
Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ
diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
new file mode 100644
index 0000000..4cd7b00
Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ
diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
new file mode 100644
index 0000000..fe73094
Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ
diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
new file mode 100644
index 0000000..321773c
Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ
diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
new file mode 100644
index 0000000..797d452
Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ
diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
new file mode 100644
index 0000000..502f463
Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ
diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
new file mode 100644
index 0000000..0ec3034
Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ
diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
new file mode 100644
index 0000000..0ec3034
Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ
diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
new file mode 100644
index 0000000..e9f5fea
Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ
diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
new file mode 100644
index 0000000..84ac32a
Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ
diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
new file mode 100644
index 0000000..8953cba
Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ
diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
new file mode 100644
index 0000000..0467bf1
Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ
diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
new file mode 100644
index 0000000..0bedcf2
--- /dev/null
+++ b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
@@ -0,0 +1,23 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "filename" : "LaunchImage.png",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "LaunchImage@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "LaunchImage@3x.png",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
new file mode 100644
index 0000000..9da19ea
Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ
diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
new file mode 100644
index 0000000..9da19ea
Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ
diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
new file mode 100644
index 0000000..9da19ea
Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ
diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
new file mode 100644
index 0000000..89c2725
--- /dev/null
+++ b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
@@ -0,0 +1,5 @@
+# Launch Screen Assets
+
+You can customize the launch screen with your own desired assets by replacing the image files in this directory.
+
+You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
\ No newline at end of file
diff --git a/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/example/ios/Runner/Base.lproj/LaunchScreen.storyboard
new file mode 100644
index 0000000..f2e259c
--- /dev/null
+++ b/example/ios/Runner/Base.lproj/LaunchScreen.storyboard
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/example/ios/Runner/Base.lproj/Main.storyboard b/example/ios/Runner/Base.lproj/Main.storyboard
new file mode 100644
index 0000000..f3c2851
--- /dev/null
+++ b/example/ios/Runner/Base.lproj/Main.storyboard
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/example/ios/Runner/GoogleService-Info.plist b/example/ios/Runner/GoogleService-Info.plist
new file mode 100644
index 0000000..39e6d39
--- /dev/null
+++ b/example/ios/Runner/GoogleService-Info.plist
@@ -0,0 +1,30 @@
+
+
+
+
+ API_KEY
+ AIzaSyCsejvqheiopTD9dsScnVro8sTDcX57dHI
+ GCM_SENDER_ID
+ 605730184710
+ PLIST_VERSION
+ 1
+ BUNDLE_ID
+ com.rees46.rees46FlutterSdkExample
+ PROJECT_ID
+ rees46-com
+ STORAGE_BUCKET
+ rees46-com.firebasestorage.app
+ IS_ADS_ENABLED
+
+ IS_ANALYTICS_ENABLED
+
+ IS_APPINVITE_ENABLED
+
+ IS_GCM_ENABLED
+
+ IS_SIGNIN_ENABLED
+
+ GOOGLE_APP_ID
+ 1:605730184710:ios:0a0e45689eec55945b6d87
+
+
\ No newline at end of file
diff --git a/example/ios/Runner/Info.plist b/example/ios/Runner/Info.plist
new file mode 100644
index 0000000..e80a01c
--- /dev/null
+++ b/example/ios/Runner/Info.plist
@@ -0,0 +1,70 @@
+
+
+
+
+ CADisableMinimumFrameDurationOnPhone
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleDisplayName
+ Rees46 Flutter Sdk
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ rees46_flutter_sdk_example
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ $(FLUTTER_BUILD_NAME)
+ CFBundleSignature
+ ????
+ CFBundleVersion
+ $(FLUTTER_BUILD_NUMBER)
+ LSRequiresIPhoneOS
+
+ UIApplicationSceneManifest
+
+ UIApplicationSupportsMultipleScenes
+
+ UISceneConfigurations
+
+ UIWindowSceneSessionRoleApplication
+
+
+ UISceneClassName
+ UIWindowScene
+ UISceneConfigurationName
+ flutter
+ UISceneDelegateClassName
+ $(PRODUCT_MODULE_NAME).SceneDelegate
+ UISceneStoryboardFile
+ Main
+
+
+
+
+ UIApplicationSupportsIndirectInputEvents
+
+ UILaunchStoryboardName
+ LaunchScreen
+ UIMainStoryboardFile
+ Main
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UISupportedInterfaceOrientations~ipad
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationPortraitUpsideDown
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+
+
diff --git a/example/ios/Runner/Runner-Bridging-Header.h b/example/ios/Runner/Runner-Bridging-Header.h
new file mode 100644
index 0000000..308a2a5
--- /dev/null
+++ b/example/ios/Runner/Runner-Bridging-Header.h
@@ -0,0 +1 @@
+#import "GeneratedPluginRegistrant.h"
diff --git a/example/ios/Runner/SceneDelegate.swift b/example/ios/Runner/SceneDelegate.swift
new file mode 100644
index 0000000..b9ce8ea
--- /dev/null
+++ b/example/ios/Runner/SceneDelegate.swift
@@ -0,0 +1,6 @@
+import Flutter
+import UIKit
+
+class SceneDelegate: FlutterSceneDelegate {
+
+}
diff --git a/example/ios/RunnerTests/RunnerTests.swift b/example/ios/RunnerTests/RunnerTests.swift
new file mode 100644
index 0000000..2d79764
--- /dev/null
+++ b/example/ios/RunnerTests/RunnerTests.swift
@@ -0,0 +1,7 @@
+import XCTest
+
+final class RunnerTests: XCTestCase {
+ func testPlaceholder() {
+ XCTAssertTrue(true)
+ }
+}
diff --git a/example/ios/RunnerUITests/RunnerUITests.m b/example/ios/RunnerUITests/RunnerUITests.m
new file mode 100644
index 0000000..8989957
--- /dev/null
+++ b/example/ios/RunnerUITests/RunnerUITests.m
@@ -0,0 +1,5 @@
+#import
+#import
+
+PATROL_INTEGRATION_TEST_IOS_RUNNER(RunnerUITests);
+
diff --git a/example/lib/main.dart b/example/lib/main.dart
new file mode 100644
index 0000000..2b9e36b
--- /dev/null
+++ b/example/lib/main.dart
@@ -0,0 +1,1089 @@
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter/foundation.dart';
+import 'package:personalization_flutter_sdk/personalization_flutter_sdk.dart';
+
+void main() => runApp(const App());
+
+class App extends StatelessWidget {
+ const App({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return MaterialApp(
+ title: 'REES46 Flutter SDK',
+ theme: ThemeData(
+ colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
+ ),
+ home: const InitPage(),
+ );
+ }
+}
+
+class InitPage extends StatefulWidget {
+ const InitPage({super.key});
+
+ @override
+ State createState() => _InitPageState();
+}
+
+enum InitState { idle, initializing, initialized, failed }
+
+class _InitPageState extends State {
+ final _sdk = PersonalizationSdk();
+
+ // Hardcoded demo credentials — the demo does not need editable init inputs.
+ static const _shopId = '357382bf66ac0ce2f1722677c59511';
+ static const _apiDomain = 'api.rees46.ru';
+ final _stream = defaultTargetPlatform == TargetPlatform.android
+ ? 'android'
+ : 'ios';
+
+ bool _enableLogs = false;
+ bool _autoSendPushToken = true;
+ bool _sendAdvertisingId = false;
+ bool _enableAutoPopupPresentation = true;
+ bool _needReInitialization = false;
+
+ InitState _initState = InitState.idle;
+ String? _initError;
+ DateTime? _lastInitAt;
+
+ String? _storedPushToken;
+ DateTime? _tokenUpdatedAt;
+ bool _tokenLoading = false;
+
+ // Profile & session state
+ String? _sid;
+ String? _did;
+ String? _profileStatus;
+
+ // Recommendation state
+ final _recBlockController = TextEditingController();
+ String? _recTitle;
+ int? _recProductCount;
+ String? _recError;
+ bool _recLoading = false;
+
+ // Search (full) state
+ final _searchQueryController = TextEditingController();
+ int? _searchTotal;
+ String? _searchError;
+ bool _searchLoading = false;
+
+ // Product info state
+ final _productIdController = TextEditingController();
+ String? _productInfoName;
+ String? _productInfoError;
+ bool _productInfoLoading = false;
+
+ // Products list state
+ int? _productsListTotal;
+ String? _productsListError;
+ bool _productsListLoading = false;
+
+ // Search blank state
+ int? _searchBlankProductCount;
+ int? _searchBlankSuggestCount;
+ String? _searchBlankError;
+ bool _searchBlankLoading = false;
+
+ // Search instant state
+ final _searchInstantQueryController = TextEditingController();
+ int? _searchInstantTotal;
+ String? _searchInstantError;
+ bool _searchInstantLoading = false;
+
+ @override
+ void dispose() {
+ _recBlockController.dispose();
+ _productIdController.dispose();
+ _searchQueryController.dispose();
+ _searchInstantQueryController.dispose();
+ super.dispose();
+ }
+
+ @override
+ void initState() {
+ super.initState();
+ _sdk.setPushNotificationCallbacks(
+ onReceived: (payload) {
+ // In the demo we only surface init/token state; push payloads can be added later.
+ },
+ onDelivered: (payload) {},
+ onClicked: (payload) {},
+ );
+ // Auto-initialize on startup — the demo uses hardcoded config, so no manual
+ // step is needed. The button below only re-initializes (e.g. after toggling flags).
+ _initialize();
+ }
+
+ Future _initialize() async {
+ setState(() {
+ _initState = InitState.initializing;
+ _initError = null;
+ });
+
+ try {
+ await _sdk.initialize(
+ SdkInitConfig(
+ shopId: _shopId,
+ apiDomain: _apiDomain,
+ stream: _stream,
+ enableLogs: _enableLogs,
+ autoSendPushToken: _autoSendPushToken,
+ sendAdvertisingId: _sendAdvertisingId,
+ enableAutoPopupPresentation: _enableAutoPopupPresentation,
+ needReInitialization: _needReInitialization,
+ ),
+ );
+ setState(() {
+ _initState = InitState.initialized;
+ _lastInitAt = DateTime.now();
+ });
+ await _refreshToken();
+ } on PlatformException catch (e) {
+ setState(() {
+ _initState = InitState.failed;
+ _initError = 'PlatformException: ${e.code} ${e.message ?? ''}'.trim();
+ });
+ } catch (e) {
+ setState(() {
+ _initState = InitState.failed;
+ _initError = 'Error: $e';
+ });
+ }
+ }
+
+ Future _refreshToken() async {
+ setState(() => _tokenLoading = true);
+ try {
+ final token = await _sdk.getStoredPushToken();
+ setState(() {
+ _storedPushToken = (token == null || token.trim().isEmpty)
+ ? null
+ : token.trim();
+ _tokenUpdatedAt = DateTime.now();
+ });
+ } catch (e) {
+ // Keep token as-is; this is just a demo screen.
+ } finally {
+ setState(() => _tokenLoading = false);
+ }
+ }
+
+ Future _getSid() async {
+ try {
+ final sid = await _sdk.getSid();
+ setState(() => _sid = sid);
+ } catch (e) {
+ setState(() => _sid = 'Error: $e');
+ }
+ }
+
+ Future _getDid() async {
+ try {
+ final did = await _sdk.getDid();
+ setState(() => _did = did ?? 'null');
+ } catch (e) {
+ setState(() => _did = 'Error: $e');
+ }
+ }
+
+ Future _setProfile() async {
+ try {
+ await _sdk.setProfile(
+ const ProfileParams(
+ email: 'test@example.com',
+ firstName: 'Test',
+ gender: ProfileGender.male,
+ ),
+ );
+ setState(() => _profileStatus = 'Profile set');
+ } catch (e) {
+ setState(() => _profileStatus = 'Error: $e');
+ }
+ }
+
+ Future _getRecommendation() async {
+ final code = _recBlockController.text.trim();
+ if (code.isEmpty) return;
+ setState(() {
+ _recLoading = true;
+ _recError = null;
+ _recTitle = null;
+ _recProductCount = null;
+ });
+ try {
+ final response = await _sdk.getRecommendation(code);
+ setState(() {
+ _recTitle = response.title;
+ _recProductCount = response.products.length;
+ _recLoading = false;
+ });
+ } catch (e) {
+ setState(() {
+ _recError = 'Error: $e';
+ _recLoading = false;
+ });
+ }
+ }
+
+ Future _searchFull() async {
+ final query = _searchQueryController.text.trim();
+ if (query.isEmpty) return;
+ setState(() {
+ _searchLoading = true;
+ _searchError = null;
+ _searchTotal = null;
+ });
+ try {
+ final response = await _sdk.searchFull(query);
+ setState(() {
+ _searchTotal = response.productsTotal;
+ _searchLoading = false;
+ });
+ } catch (e) {
+ setState(() {
+ _searchError = 'Error: $e';
+ _searchLoading = false;
+ });
+ }
+ }
+
+ Future _getProductInfo() async {
+ final id = _productIdController.text.trim();
+ if (id.isEmpty) return;
+ setState(() {
+ _productInfoLoading = true;
+ _productInfoError = null;
+ _productInfoName = null;
+ });
+ try {
+ final product = await _sdk.getProductInfo(id);
+ setState(() {
+ _productInfoName = product.name;
+ _productInfoLoading = false;
+ });
+ } catch (e) {
+ setState(() {
+ _productInfoError = 'Error: $e';
+ _productInfoLoading = false;
+ });
+ }
+ }
+
+ Future _getProductsList() async {
+ setState(() {
+ _productsListLoading = true;
+ _productsListError = null;
+ _productsListTotal = null;
+ });
+ try {
+ final response = await _sdk.getProductsList();
+ setState(() {
+ _productsListTotal = response.productsTotal;
+ _productsListLoading = false;
+ });
+ } catch (e) {
+ setState(() {
+ _productsListError = 'Error: $e';
+ _productsListLoading = false;
+ });
+ }
+ }
+
+ Future _searchBlank() async {
+ setState(() {
+ _searchBlankLoading = true;
+ _searchBlankError = null;
+ _searchBlankProductCount = null;
+ _searchBlankSuggestCount = null;
+ });
+ try {
+ final response = await _sdk.searchBlank();
+ setState(() {
+ _searchBlankProductCount = response.products.length;
+ _searchBlankSuggestCount = response.suggests.length;
+ _searchBlankLoading = false;
+ });
+ } catch (e) {
+ setState(() {
+ _searchBlankError = 'Error: $e';
+ _searchBlankLoading = false;
+ });
+ }
+ }
+
+ Future _searchInstant() async {
+ final query = _searchInstantQueryController.text.trim();
+ if (query.isEmpty) return;
+ setState(() {
+ _searchInstantLoading = true;
+ _searchInstantError = null;
+ _searchInstantTotal = null;
+ });
+ try {
+ final response = await _sdk.searchInstant(query);
+ setState(() {
+ _searchInstantTotal = response.productsTotal;
+ _searchInstantLoading = false;
+ });
+ } catch (e) {
+ setState(() {
+ _searchInstantError = 'Error: $e';
+ _searchInstantLoading = false;
+ });
+ }
+ }
+
+ Future _demoTrackEvent() async {
+ if (_initState != InitState.initialized) return;
+ try {
+ await _sdk.trackEvent(
+ 'flutter_example',
+ customFields: const {'source': 'example_app'},
+ );
+ if (!mounted) return;
+ ScaffoldMessenger.of(
+ context,
+ ).showSnackBar(const SnackBar(content: Text('trackEvent sent')));
+ } catch (e) {
+ if (!mounted) return;
+ ScaffoldMessenger.of(
+ context,
+ ).showSnackBar(SnackBar(content: Text('trackEvent failed: $e')));
+ }
+ }
+
+ Future _demoTrackPurchase() async {
+ if (_initState != InitState.initialized) return;
+ try {
+ await _sdk.trackPurchase(
+ orderId: 'example-order-1',
+ orderPrice: 99.0,
+ items: const [PurchaseLineItem(id: 'sku-1', amount: 1, price: 99.0)],
+ );
+ if (!mounted) return;
+ ScaffoldMessenger.of(
+ context,
+ ).showSnackBar(const SnackBar(content: Text('trackPurchase sent')));
+ } catch (e) {
+ if (!mounted) return;
+ ScaffoldMessenger.of(
+ context,
+ ).showSnackBar(SnackBar(content: Text('trackPurchase failed: $e')));
+ }
+ }
+
+ Future _copyToken() async {
+ final token = _storedPushToken;
+ if (token == null || token.isEmpty) return;
+ await Clipboard.setData(ClipboardData(text: token));
+ if (!mounted) return;
+ ScaffoldMessenger.of(
+ context,
+ ).showSnackBar(const SnackBar(content: Text('Token copied')));
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ appBar: AppBar(title: const Text('REES46 SDK init demo')),
+ body: ListView(
+ padding: const EdgeInsets.all(16),
+ children: [
+ _InitStatusCard(
+ state: _initState,
+ error: _initError,
+ lastInitAt: _lastInitAt,
+ ),
+ const SizedBox(height: 12),
+ _PushTokenCard(
+ token: _storedPushToken,
+ updatedAt: _tokenUpdatedAt,
+ loading: _tokenLoading,
+ onRefresh: _tokenLoading ? null : _refreshToken,
+ onCopy: (_storedPushToken?.isNotEmpty ?? false) ? _copyToken : null,
+ ),
+ const SizedBox(height: 24),
+ SwitchListTile(
+ value: _enableLogs,
+ onChanged: (v) => setState(() => _enableLogs = v),
+ title: const Text('enableLogs (iOS)'),
+ ),
+ SwitchListTile(
+ value: _autoSendPushToken,
+ onChanged: (v) => setState(() => _autoSendPushToken = v),
+ title: const Text('autoSendPushToken'),
+ ),
+ SwitchListTile(
+ value: _sendAdvertisingId,
+ onChanged: (v) => setState(() => _sendAdvertisingId = v),
+ title: const Text('sendAdvertisingId (iOS)'),
+ ),
+ SwitchListTile(
+ value: _enableAutoPopupPresentation,
+ onChanged: (v) => setState(() => _enableAutoPopupPresentation = v),
+ title: const Text('enableAutoPopupPresentation (iOS)'),
+ ),
+ SwitchListTile(
+ value: _needReInitialization,
+ onChanged: (v) => setState(() => _needReInitialization = v),
+ title: const Text('needReInitialization'),
+ ),
+ const SizedBox(height: 16),
+ FilledButton(
+ onPressed: _initState == InitState.initializing
+ ? null
+ : _initialize,
+ child: Text(
+ _initState == InitState.initializing
+ ? 'Initializing…'
+ : 'Re-initialize',
+ ),
+ ),
+ const SizedBox(height: 12),
+ _ProfileCard(
+ sid: _sid,
+ did: _did,
+ profileStatus: _profileStatus,
+ enabled: _initState == InitState.initialized,
+ onGetSid: _getSid,
+ onGetDid: _getDid,
+ onSetProfile: _setProfile,
+ ),
+ const SizedBox(height: 12),
+ _RecommendationCard(
+ blockController: _recBlockController,
+ title: _recTitle,
+ productCount: _recProductCount,
+ error: _recError,
+ loading: _recLoading,
+ enabled: _initState == InitState.initialized,
+ onGet: _getRecommendation,
+ ),
+ const SizedBox(height: 12),
+ _ProductInfoCard(
+ idController: _productIdController,
+ productName: _productInfoName,
+ error: _productInfoError,
+ loading: _productInfoLoading,
+ enabled: _initState == InitState.initialized,
+ onGet: _getProductInfo,
+ ),
+ const SizedBox(height: 12),
+ _ProductsListCard(
+ total: _productsListTotal,
+ error: _productsListError,
+ loading: _productsListLoading,
+ enabled: _initState == InitState.initialized,
+ onGet: _getProductsList,
+ ),
+ const SizedBox(height: 12),
+ _SearchBlankCard(
+ productCount: _searchBlankProductCount,
+ suggestCount: _searchBlankSuggestCount,
+ error: _searchBlankError,
+ loading: _searchBlankLoading,
+ enabled: _initState == InitState.initialized,
+ onSearch: _searchBlank,
+ ),
+ const SizedBox(height: 12),
+ _SearchInstantCard(
+ queryController: _searchInstantQueryController,
+ total: _searchInstantTotal,
+ error: _searchInstantError,
+ loading: _searchInstantLoading,
+ enabled: _initState == InitState.initialized,
+ onSearch: _searchInstant,
+ ),
+ const SizedBox(height: 12),
+ _SearchCard(
+ queryController: _searchQueryController,
+ total: _searchTotal,
+ error: _searchError,
+ loading: _searchLoading,
+ enabled: _initState == InitState.initialized,
+ onSearch: _searchFull,
+ ),
+ const SizedBox(height: 24),
+ Text('Tracking', style: Theme.of(context).textTheme.titleMedium),
+ const SizedBox(height: 8),
+ Text(
+ 'Requires successful initialization above.',
+ style: Theme.of(context).textTheme.bodySmall,
+ ),
+ const SizedBox(height: 12),
+ OutlinedButton(
+ key: const Key('example_demo_track_event'),
+ onPressed: _initState == InitState.initialized
+ ? _demoTrackEvent
+ : null,
+ child: const Text('Send demo trackEvent'),
+ ),
+ const SizedBox(height: 8),
+ OutlinedButton(
+ key: const Key('example_demo_track_purchase'),
+ onPressed: _initState == InitState.initialized
+ ? _demoTrackPurchase
+ : null,
+ child: const Text('Send demo trackPurchase'),
+ ),
+ ],
+ ),
+ );
+ }
+}
+
+class _InitStatusCard extends StatelessWidget {
+ final InitState state;
+ final String? error;
+ final DateTime? lastInitAt;
+
+ const _InitStatusCard({
+ required this.state,
+ required this.error,
+ required this.lastInitAt,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ final statusText = switch (state) {
+ InitState.idle => 'Idle',
+ InitState.initializing => 'Initializing…',
+ InitState.initialized => 'Initialized',
+ InitState.failed => 'Failed',
+ };
+
+ return Card(
+ child: Padding(
+ padding: const EdgeInsets.all(16),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ 'Initialization',
+ style: Theme.of(context).textTheme.titleMedium,
+ ),
+ const SizedBox(height: 8),
+ Text('Status: $statusText'),
+ if (lastInitAt != null)
+ Text('Last init: ${lastInitAt!.toIso8601String()}'),
+ if (error != null) ...[
+ const SizedBox(height: 8),
+ Text(
+ error!,
+ style: TextStyle(color: Theme.of(context).colorScheme.error),
+ ),
+ ],
+ ],
+ ),
+ ),
+ );
+ }
+}
+
+class _ProfileCard extends StatelessWidget {
+ final String? sid;
+ final String? did;
+ final String? profileStatus;
+ final bool enabled;
+ final VoidCallback onGetSid;
+ final VoidCallback onGetDid;
+ final VoidCallback onSetProfile;
+
+ const _ProfileCard({
+ required this.sid,
+ required this.did,
+ required this.profileStatus,
+ required this.enabled,
+ required this.onGetSid,
+ required this.onGetDid,
+ required this.onSetProfile,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return Card(
+ child: Padding(
+ padding: const EdgeInsets.all(16),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ 'Profile & Session',
+ style: Theme.of(context).textTheme.titleMedium,
+ ),
+ const SizedBox(height: 12),
+ Row(
+ children: [
+ OutlinedButton(
+ key: const Key('btn_get_sid'),
+ onPressed: enabled ? onGetSid : null,
+ child: const Text('Get SID'),
+ ),
+ const SizedBox(width: 8),
+ Expanded(
+ child: Text(
+ key: const Key('lbl_sid'),
+ sid ?? '—',
+ overflow: TextOverflow.ellipsis,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 8),
+ Row(
+ children: [
+ OutlinedButton(
+ key: const Key('btn_get_did'),
+ onPressed: enabled ? onGetDid : null,
+ child: const Text('Get DID'),
+ ),
+ const SizedBox(width: 8),
+ Expanded(
+ child: Text(
+ key: const Key('lbl_did'),
+ did ?? '—',
+ overflow: TextOverflow.ellipsis,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 8),
+ Row(
+ children: [
+ OutlinedButton(
+ key: const Key('btn_set_profile'),
+ onPressed: enabled ? onSetProfile : null,
+ child: const Text('Set Profile'),
+ ),
+ const SizedBox(width: 8),
+ if (profileStatus != null)
+ Text(key: const Key('lbl_profile_status'), profileStatus!),
+ ],
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
+
+class _RecommendationCard extends StatelessWidget {
+ final TextEditingController blockController;
+ final String? title;
+ final int? productCount;
+ final String? error;
+ final bool loading;
+ final bool enabled;
+ final VoidCallback onGet;
+
+ const _RecommendationCard({
+ required this.blockController,
+ required this.title,
+ required this.productCount,
+ required this.error,
+ required this.loading,
+ required this.enabled,
+ required this.onGet,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return Card(
+ child: Padding(
+ padding: const EdgeInsets.all(16),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ 'Recommendations',
+ style: Theme.of(context).textTheme.titleMedium,
+ ),
+ const SizedBox(height: 12),
+ TextField(
+ key: const Key('field_rec_block_code'),
+ controller: blockController,
+ decoration: const InputDecoration(
+ labelText: 'Block code',
+ hintText: 'e.g. main_page_2',
+ ),
+ ),
+ const SizedBox(height: 12),
+ OutlinedButton(
+ key: const Key('btn_get_recommendations'),
+ onPressed: (enabled && !loading) ? onGet : null,
+ child: Text(loading ? 'Loading…' : 'Get Recommendations'),
+ ),
+ if (title != null) ...[
+ const SizedBox(height: 8),
+ Text(key: const Key('lbl_rec_title'), 'Title: $title'),
+ Text(key: const Key('lbl_rec_count'), 'Products: $productCount'),
+ ],
+ if (error != null) ...[
+ const SizedBox(height: 8),
+ Text(
+ key: const Key('lbl_rec_error'),
+ error!,
+ style: TextStyle(color: Theme.of(context).colorScheme.error),
+ ),
+ ],
+ ],
+ ),
+ ),
+ );
+ }
+}
+
+class _ProductInfoCard extends StatelessWidget {
+ final TextEditingController idController;
+ final String? productName;
+ final String? error;
+ final bool loading;
+ final bool enabled;
+ final VoidCallback onGet;
+
+ const _ProductInfoCard({
+ required this.idController,
+ required this.productName,
+ required this.error,
+ required this.loading,
+ required this.enabled,
+ required this.onGet,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return Card(
+ child: Padding(
+ padding: const EdgeInsets.all(16),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ 'Product Info',
+ style: Theme.of(context).textTheme.titleMedium,
+ ),
+ const SizedBox(height: 12),
+ TextField(
+ key: const Key('field_product_id'),
+ controller: idController,
+ decoration: const InputDecoration(
+ labelText: 'Product ID',
+ hintText: 'e.g. sku-123',
+ ),
+ ),
+ const SizedBox(height: 12),
+ OutlinedButton(
+ key: const Key('btn_get_product_info'),
+ onPressed: (enabled && !loading) ? onGet : null,
+ child: Text(loading ? 'Loading…' : 'Get Product Info'),
+ ),
+ if (productName != null) ...[
+ const SizedBox(height: 8),
+ Text(
+ key: const Key('lbl_product_info_name'),
+ 'Name: $productName',
+ ),
+ ],
+ if (error != null) ...[
+ const SizedBox(height: 8),
+ Text(
+ key: const Key('lbl_product_info_error'),
+ error!,
+ style: TextStyle(color: Theme.of(context).colorScheme.error),
+ ),
+ ],
+ ],
+ ),
+ ),
+ );
+ }
+}
+
+class _ProductsListCard extends StatelessWidget {
+ final int? total;
+ final String? error;
+ final bool loading;
+ final bool enabled;
+ final VoidCallback onGet;
+
+ const _ProductsListCard({
+ required this.total,
+ required this.error,
+ required this.loading,
+ required this.enabled,
+ required this.onGet,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return Card(
+ child: Padding(
+ padding: const EdgeInsets.all(16),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ 'Products List',
+ style: Theme.of(context).textTheme.titleMedium,
+ ),
+ const SizedBox(height: 12),
+ OutlinedButton(
+ key: const Key('btn_get_products_list'),
+ onPressed: (enabled && !loading) ? onGet : null,
+ child: Text(loading ? 'Loading…' : 'Get Products List'),
+ ),
+ if (total != null) ...[
+ const SizedBox(height: 8),
+ Text(key: const Key('lbl_products_list_total'), 'Total: $total'),
+ ],
+ if (error != null) ...[
+ const SizedBox(height: 8),
+ Text(
+ key: const Key('lbl_products_list_error'),
+ error!,
+ style: TextStyle(color: Theme.of(context).colorScheme.error),
+ ),
+ ],
+ ],
+ ),
+ ),
+ );
+ }
+}
+
+class _SearchBlankCard extends StatelessWidget {
+ final int? productCount;
+ final int? suggestCount;
+ final String? error;
+ final bool loading;
+ final bool enabled;
+ final VoidCallback onSearch;
+
+ const _SearchBlankCard({
+ required this.productCount,
+ required this.suggestCount,
+ required this.error,
+ required this.loading,
+ required this.enabled,
+ required this.onSearch,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return Card(
+ child: Padding(
+ padding: const EdgeInsets.all(16),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ 'Search Blank',
+ style: Theme.of(context).textTheme.titleMedium,
+ ),
+ const SizedBox(height: 12),
+ OutlinedButton(
+ key: const Key('btn_search_blank'),
+ onPressed: (enabled && !loading) ? onSearch : null,
+ child: Text(loading ? 'Loading…' : 'Search Blank'),
+ ),
+ if (productCount != null) ...[
+ const SizedBox(height: 8),
+ Text(
+ key: const Key('lbl_search_blank_products'),
+ 'Products: $productCount',
+ ),
+ Text(
+ key: const Key('lbl_search_blank_suggests'),
+ 'Suggests: $suggestCount',
+ ),
+ ],
+ if (error != null) ...[
+ const SizedBox(height: 8),
+ Text(
+ key: const Key('lbl_search_blank_error'),
+ error!,
+ style: TextStyle(color: Theme.of(context).colorScheme.error),
+ ),
+ ],
+ ],
+ ),
+ ),
+ );
+ }
+}
+
+class _SearchInstantCard extends StatelessWidget {
+ final TextEditingController queryController;
+ final int? total;
+ final String? error;
+ final bool loading;
+ final bool enabled;
+ final VoidCallback onSearch;
+
+ const _SearchInstantCard({
+ required this.queryController,
+ required this.total,
+ required this.error,
+ required this.loading,
+ required this.enabled,
+ required this.onSearch,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return Card(
+ child: Padding(
+ padding: const EdgeInsets.all(16),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ 'Search Instant',
+ style: Theme.of(context).textTheme.titleMedium,
+ ),
+ const SizedBox(height: 12),
+ TextField(
+ key: const Key('field_search_instant_query'),
+ controller: queryController,
+ decoration: const InputDecoration(
+ labelText: 'Instant query',
+ hintText: 'e.g. pho',
+ ),
+ ),
+ const SizedBox(height: 12),
+ OutlinedButton(
+ key: const Key('btn_search_instant'),
+ onPressed: (enabled && !loading) ? onSearch : null,
+ child: Text(loading ? 'Searching…' : 'Search Instant'),
+ ),
+ if (total != null) ...[
+ const SizedBox(height: 8),
+ Text(key: const Key('lbl_search_instant_total'), 'Total: $total'),
+ ],
+ if (error != null) ...[
+ const SizedBox(height: 8),
+ Text(
+ key: const Key('lbl_search_instant_error'),
+ error!,
+ style: TextStyle(color: Theme.of(context).colorScheme.error),
+ ),
+ ],
+ ],
+ ),
+ ),
+ );
+ }
+}
+
+class _SearchCard extends StatelessWidget {
+ final TextEditingController queryController;
+ final int? total;
+ final String? error;
+ final bool loading;
+ final bool enabled;
+ final VoidCallback onSearch;
+
+ const _SearchCard({
+ required this.queryController,
+ required this.total,
+ required this.error,
+ required this.loading,
+ required this.enabled,
+ required this.onSearch,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return Card(
+ child: Padding(
+ padding: const EdgeInsets.all(16),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text('Search', style: Theme.of(context).textTheme.titleMedium),
+ const SizedBox(height: 12),
+ TextField(
+ key: const Key('field_search_query'),
+ controller: queryController,
+ decoration: const InputDecoration(
+ labelText: 'Search query',
+ hintText: 'e.g. phone',
+ ),
+ ),
+ const SizedBox(height: 12),
+ OutlinedButton(
+ key: const Key('btn_search'),
+ onPressed: (enabled && !loading) ? onSearch : null,
+ child: Text(loading ? 'Searching…' : 'Search'),
+ ),
+ if (total != null) ...[
+ const SizedBox(height: 8),
+ Text(key: const Key('lbl_search_total'), 'Total: $total'),
+ ],
+ if (error != null) ...[
+ const SizedBox(height: 8),
+ Text(
+ key: const Key('lbl_search_error'),
+ error!,
+ style: TextStyle(color: Theme.of(context).colorScheme.error),
+ ),
+ ],
+ ],
+ ),
+ ),
+ );
+ }
+}
+
+class _PushTokenCard extends StatelessWidget {
+ final String? token;
+ final DateTime? updatedAt;
+ final bool loading;
+ final VoidCallback? onRefresh;
+ final VoidCallback? onCopy;
+
+ const _PushTokenCard({
+ required this.token,
+ required this.updatedAt,
+ required this.loading,
+ required this.onRefresh,
+ required this.onCopy,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return Card(
+ child: Padding(
+ padding: const EdgeInsets.all(16),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ 'Stored push token',
+ style: Theme.of(context).textTheme.titleMedium,
+ ),
+ const SizedBox(height: 8),
+ Text(token ?? 'Not available yet'),
+ if (updatedAt != null)
+ Text('Updated: ${updatedAt!.toIso8601String()}'),
+ const SizedBox(height: 12),
+ Row(
+ children: [
+ OutlinedButton(
+ onPressed: onRefresh,
+ child: Text(loading ? 'Refreshing…' : 'Refresh'),
+ ),
+ const SizedBox(width: 12),
+ FilledButton(onPressed: onCopy, child: const Text('Copy')),
+ ],
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
diff --git a/example/plugin_integration_test.dart b/example/plugin_integration_test.dart
new file mode 100644
index 0000000..330518c
--- /dev/null
+++ b/example/plugin_integration_test.dart
@@ -0,0 +1,24 @@
+// This is a basic Flutter integration test.
+//
+// Since integration tests run in a full Flutter application, they can interact
+// with the host side of a plugin implementation, unlike Dart unit tests.
+//
+// For more information about Flutter integration tests, please see
+// https://flutter.dev/to/integration-testing
+
+import 'package:flutter_test/flutter_test.dart';
+import 'package:integration_test/integration_test.dart';
+
+import 'package:personalization_flutter_sdk/personaclick_flutter_sdk.dart';
+
+void main() {
+ IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+
+ testWidgets('getPlatformVersion test', (WidgetTester tester) async {
+ final PersonaclickFlutterSdk plugin = PersonaclickFlutterSdk();
+ final String? version = await plugin.getPlatformVersion();
+ // The version string depends on the host platform running the test, so
+ // just assert that some non-empty string is returned.
+ expect(version?.isNotEmpty, true);
+ });
+}
diff --git a/example/pubspec.lock b/example/pubspec.lock
new file mode 100644
index 0000000..2369271
--- /dev/null
+++ b/example/pubspec.lock
@@ -0,0 +1,371 @@
+# Generated by pub
+# See https://dart.dev/tools/pub/glossary#lockfile
+packages:
+ async:
+ dependency: transitive
+ description:
+ name: async
+ sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.13.1"
+ boolean_selector:
+ dependency: transitive
+ description:
+ name: boolean_selector
+ sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.1.2"
+ characters:
+ dependency: transitive
+ description:
+ name: characters
+ sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.4.1"
+ clock:
+ dependency: transitive
+ description:
+ name: clock
+ sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.1.2"
+ collection:
+ dependency: transitive
+ description:
+ name: collection
+ sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.19.1"
+ cupertino_icons:
+ dependency: "direct main"
+ description:
+ name: cupertino_icons
+ sha256: "41e005c33bd814be4d3096aff55b1908d419fde52ca656c8c47719ec745873cd"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.0.9"
+ dispose_scope:
+ dependency: transitive
+ description:
+ name: dispose_scope
+ sha256: "48ec38ca2631c53c4f8fa96b294c801e55c335db5e3fb9f82cede150cfe5a2af"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.1.0"
+ equatable:
+ dependency: transitive
+ description:
+ name: equatable
+ sha256: "3e0141505477fd8ad55d6eb4e7776d3fe8430be8e497ccb1521370c3f21a3e2b"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.0.8"
+ fake_async:
+ dependency: transitive
+ description:
+ name: fake_async
+ sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.3.3"
+ file:
+ dependency: transitive
+ description:
+ name: file
+ sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
+ url: "https://pub.dev"
+ source: hosted
+ version: "7.0.1"
+ flutter:
+ dependency: "direct main"
+ description: flutter
+ source: sdk
+ version: "0.0.0"
+ flutter_driver:
+ dependency: transitive
+ description: flutter
+ source: sdk
+ version: "0.0.0"
+ flutter_lints:
+ dependency: "direct dev"
+ description:
+ name: flutter_lints
+ sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1"
+ url: "https://pub.dev"
+ source: hosted
+ version: "6.0.0"
+ flutter_test:
+ dependency: "direct dev"
+ description: flutter
+ source: sdk
+ version: "0.0.0"
+ fuchsia_remote_debug_protocol:
+ dependency: transitive
+ description: flutter
+ source: sdk
+ version: "0.0.0"
+ http:
+ dependency: transitive
+ description:
+ name: http
+ sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.6.0"
+ http_parser:
+ dependency: transitive
+ description:
+ name: http_parser
+ sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
+ url: "https://pub.dev"
+ source: hosted
+ version: "4.1.2"
+ integration_test:
+ dependency: "direct dev"
+ description: flutter
+ source: sdk
+ version: "0.0.0"
+ json_annotation:
+ dependency: transitive
+ description:
+ name: json_annotation
+ sha256: cb09e7dac6210041fad964ed7fbee004f14258b4eca4040f72d1234062ace4c8
+ url: "https://pub.dev"
+ source: hosted
+ version: "4.11.0"
+ leak_tracker:
+ dependency: transitive
+ description:
+ name: leak_tracker
+ sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
+ url: "https://pub.dev"
+ source: hosted
+ version: "11.0.2"
+ leak_tracker_flutter_testing:
+ dependency: transitive
+ description:
+ name: leak_tracker_flutter_testing
+ sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.0.10"
+ leak_tracker_testing:
+ dependency: transitive
+ description:
+ name: leak_tracker_testing
+ sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.0.2"
+ lints:
+ dependency: transitive
+ description:
+ name: lints
+ sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df"
+ url: "https://pub.dev"
+ source: hosted
+ version: "6.1.0"
+ matcher:
+ dependency: transitive
+ description:
+ name: matcher
+ sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.12.19"
+ material_color_utilities:
+ dependency: transitive
+ description:
+ name: material_color_utilities
+ sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.13.0"
+ meta:
+ dependency: transitive
+ description:
+ name: meta
+ sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.17.0"
+ path:
+ dependency: transitive
+ description:
+ name: path
+ sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.9.1"
+ patrol:
+ dependency: "direct dev"
+ description:
+ name: patrol
+ sha256: "7825a6e96a8f0755f68eec600a91a08b19bd0975488a70885b3696f6b65ffc0f"
+ url: "https://pub.dev"
+ source: hosted
+ version: "4.5.0"
+ patrol_finders:
+ dependency: transitive
+ description:
+ name: patrol_finders
+ sha256: "9970eac0669a90b20ec7e1bcaabd0475655655998068ca656f4df9f6ec84f336"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.2.0"
+ patrol_log:
+ dependency: transitive
+ description:
+ name: patrol_log
+ sha256: a2360db165c34692665c0de146e5157887d6b584fdccca8f141f947a5acf1b2e
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.8.0"
+ personalization_flutter_sdk:
+ dependency: "direct main"
+ description:
+ path: ".."
+ relative: true
+ source: path
+ version: "0.0.1"
+ platform:
+ dependency: transitive
+ description:
+ name: platform
+ sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.1.6"
+ plugin_platform_interface:
+ dependency: transitive
+ description:
+ name: plugin_platform_interface
+ sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.1.8"
+ process:
+ dependency: transitive
+ description:
+ name: process
+ sha256: c6248e4526673988586e8c00bb22a49210c258dc91df5227d5da9748ecf79744
+ url: "https://pub.dev"
+ source: hosted
+ version: "5.0.5"
+ shelf:
+ dependency: transitive
+ description:
+ name: shelf
+ sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.4.2"
+ sky_engine:
+ dependency: transitive
+ description: flutter
+ source: sdk
+ version: "0.0.0"
+ source_span:
+ dependency: transitive
+ description:
+ name: source_span
+ sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.10.2"
+ stack_trace:
+ dependency: transitive
+ description:
+ name: stack_trace
+ sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.12.1"
+ stream_channel:
+ dependency: transitive
+ description:
+ name: stream_channel
+ sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.1.4"
+ string_scanner:
+ dependency: transitive
+ description:
+ name: string_scanner
+ sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.4.1"
+ sync_http:
+ dependency: transitive
+ description:
+ name: sync_http
+ sha256: "7f0cd72eca000d2e026bcd6f990b81d0ca06022ef4e32fb257b30d3d1014a961"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.3.1"
+ term_glyph:
+ dependency: transitive
+ description:
+ name: term_glyph
+ sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.2.2"
+ test_api:
+ dependency: transitive
+ description:
+ name: test_api
+ sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.7.10"
+ typed_data:
+ dependency: transitive
+ description:
+ name: typed_data
+ sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.4.0"
+ vector_math:
+ dependency: transitive
+ description:
+ name: vector_math
+ sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.2.0"
+ vm_service:
+ dependency: transitive
+ description:
+ name: vm_service
+ sha256: "046d3928e16fa4dc46e8350415661755ab759d9fc97fc21b5ab295f71e4f0499"
+ url: "https://pub.dev"
+ source: hosted
+ version: "15.1.0"
+ web:
+ dependency: transitive
+ description:
+ name: web
+ sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.1.1"
+ webdriver:
+ dependency: transitive
+ description:
+ name: webdriver
+ sha256: "2f3a14ca026957870cfd9c635b83507e0e51d8091568e90129fbf805aba7cade"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.1.0"
+sdks:
+ dart: ">=3.11.1 <4.0.0"
+ flutter: ">=3.32.0"
diff --git a/example/pubspec.yaml b/example/pubspec.yaml
new file mode 100644
index 0000000..655e0d1
--- /dev/null
+++ b/example/pubspec.yaml
@@ -0,0 +1,94 @@
+name: personalization_flutter_sdk_example
+description: "Demonstrates how to use the personalization_flutter_sdk plugin."
+# The following line prevents the package from being accidentally published to
+# pub.dev using `flutter pub publish`. This is preferred for private packages.
+publish_to: 'none' # Remove this line if you wish to publish to pub.dev
+
+environment:
+ sdk: ^3.11.1
+
+# Dependencies specify other packages that your package needs in order to work.
+# To automatically upgrade your package dependencies to the latest versions
+# consider running `flutter pub upgrade --major-versions`. Alternatively,
+# dependencies can be manually updated by changing the version numbers below to
+# the latest version available on pub.dev. To see which dependencies have newer
+# versions available, run `flutter pub outdated`.
+dependencies:
+ flutter:
+ sdk: flutter
+
+ personalization_flutter_sdk:
+ # When depending on this package from a real application you should use:
+ # personalization_flutter_sdk: ^x.y.z
+ # See https://dart.dev/tools/pub/dependencies#version-constraints
+ # The example app is bundled with the plugin so we use a path dependency on
+ # the parent directory to use the current plugin's version.
+ path: ../
+
+ # The following adds the Cupertino Icons font to your application.
+ # Use with the CupertinoIcons class for iOS style icons.
+ cupertino_icons: ^1.0.8
+
+dev_dependencies:
+ integration_test:
+ sdk: flutter
+ flutter_test:
+ sdk: flutter
+
+ # The "flutter_lints" package below contains a set of recommended lints to
+ # encourage good coding practices. The lint set provided by the package is
+ # activated in the `analysis_options.yaml` file located at the root of your
+ # package. See that file for information about deactivating specific lint
+ # rules and activating additional ones.
+ flutter_lints: ^6.0.0
+ patrol: ^4.5.0
+
+patrol:
+ app_name: personalization_flutter_sdk_example
+ test_directory: integration_test
+ android:
+ package_name: com.rees46.rees46_flutter_sdk_example
+ ios:
+ bundle_id: com.rees46.rees46FlutterSdkExample
+
+# For information on the generic Dart part of this file, see the
+# following page: https://dart.dev/tools/pub/pubspec
+
+# The following section is specific to Flutter packages.
+flutter:
+
+ # The following line ensures that the Material Icons font is
+ # included with your application, so that you can use the icons in
+ # the material Icons class.
+ uses-material-design: true
+
+ # To add assets to your application, add an assets section, like this:
+ # assets:
+ # - images/a_dot_burr.jpeg
+ # - images/a_dot_ham.jpeg
+
+ # An image asset can refer to one or more resolution-specific "variants", see
+ # https://flutter.dev/to/resolution-aware-images
+
+ # For details regarding adding assets from package dependencies, see
+ # https://flutter.dev/to/asset-from-package
+
+ # To add custom fonts to your application, add a fonts section here,
+ # in this "flutter" section. Each entry in this list should have a
+ # "family" key with the font family name, and a "fonts" key with a
+ # list giving the asset and other descriptors for the font. For
+ # example:
+ # fonts:
+ # - family: Schyler
+ # fonts:
+ # - asset: fonts/Schyler-Regular.ttf
+ # - asset: fonts/Schyler-Italic.ttf
+ # style: italic
+ # - family: Trajan Pro
+ # fonts:
+ # - asset: fonts/TrajanPro.ttf
+ # - asset: fonts/TrajanPro_Bold.ttf
+ # weight: 700
+ #
+ # For details regarding fonts from package dependencies,
+ # see https://flutter.dev/to/font-from-package
diff --git a/example/test/widget_test.dart b/example/test/widget_test.dart
new file mode 100644
index 0000000..1a8e20c
--- /dev/null
+++ b/example/test/widget_test.dart
@@ -0,0 +1,17 @@
+// This is a basic Flutter widget test.
+//
+// To perform an interaction with a widget in your test, use the WidgetTester
+// utility in the flutter_test package. For example, you can send tap and scroll
+// gestures. You can also use WidgetTester to find child widgets in the widget
+// tree, read text, and verify that the values of widget properties are correct.
+
+import 'package:flutter_test/flutter_test.dart';
+
+import 'package:personalization_flutter_sdk_example/main.dart';
+
+void main() {
+ testWidgets('Renders init demo page', (WidgetTester tester) async {
+ await tester.pumpWidget(const App());
+ expect(find.text('REES46 SDK init demo'), findsOneWidget);
+ });
+}
diff --git a/ios/.gitignore b/ios/.gitignore
new file mode 100644
index 0000000..034771f
--- /dev/null
+++ b/ios/.gitignore
@@ -0,0 +1,38 @@
+.idea/
+.vagrant/
+.sconsign.dblite
+.svn/
+
+.DS_Store
+*.swp
+profile
+
+DerivedData/
+build/
+GeneratedPluginRegistrant.h
+GeneratedPluginRegistrant.m
+
+.generated/
+
+*.pbxuser
+*.mode1v3
+*.mode2v3
+*.perspectivev3
+
+!default.pbxuser
+!default.mode1v3
+!default.mode2v3
+!default.perspectivev3
+
+xcuserdata
+
+*.moved-aside
+
+*.pyc
+*sync/
+Icon?
+.tags*
+
+/Flutter/Generated.xcconfig
+/Flutter/ephemeral/
+/Flutter/flutter_export_environment.sh
diff --git a/ios/Assets/.gitkeep b/ios/Assets/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/ios/Classes/Rees46FlutterSdkPlugin.swift b/ios/Classes/Rees46FlutterSdkPlugin.swift
new file mode 100644
index 0000000..6dc7d3d
--- /dev/null
+++ b/ios/Classes/Rees46FlutterSdkPlugin.swift
@@ -0,0 +1,710 @@
+import Flutter
+import UIKit
+import REES46
+import Foundation
+import UserNotifications
+
+public class Rees46FlutterSdkPlugin: NSObject, FlutterPlugin, FlutterApplicationLifeCycleDelegate {
+ static var sdk: PersonalizationSDK?
+ static var notificationService: NotificationServiceProtocol?
+ static let pushTokenKey = "rees46_flutter_push_token"
+ private var messenger: FlutterBinaryMessenger?
+ private var api: PersonalizationHostApiImpl?
+ private var flutterApi: PersonalizationFlutterApi?
+
+ public static func register(with registrar: FlutterPluginRegistrar) {
+ let instance = Rees46FlutterSdkPlugin()
+ instance.messenger = registrar.messenger()
+ instance.api = PersonalizationHostApiImpl()
+ instance.flutterApi = PersonalizationFlutterApi(binaryMessenger: registrar.messenger())
+ PersonalizationHostApiSetup.setUp(
+ binaryMessenger: registrar.messenger(),
+ api: instance.api
+ )
+ registrar.addApplicationDelegate(instance)
+
+ if #available(iOS 10.0, *) {
+ UNUserNotificationCenter.current().delegate = instance
+ }
+ }
+
+ public func application(
+ _ application: UIApplication,
+ didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
+ ) {
+ UserDefaults.standard.set(deviceToken, forKey: Rees46FlutterSdkPlugin.pushTokenKey)
+ Rees46FlutterSdkPlugin.notificationService?
+ .didRegisterForRemoteNotificationsWithDeviceToken(deviceToken: deviceToken)
+ }
+
+ public func application(
+ _ application: UIApplication,
+ didReceiveRemoteNotification userInfo: [AnyHashable: Any],
+ fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void
+ ) -> Bool {
+ flutterApi?.onPushReceived(payload: Self._stringPayload(userInfo)) { _ in }
+
+ Rees46FlutterSdkPlugin.notificationService?
+ .didReceiveRemoteNotifications(application, didReceiveRemoteNotification: userInfo) { result, _ in
+ completionHandler(result)
+ }
+ return true
+ }
+}
+
+@available(iOS 10.0, *)
+extension Rees46FlutterSdkPlugin: UNUserNotificationCenterDelegate {
+ public func userNotificationCenter(
+ _ center: UNUserNotificationCenter,
+ didReceive response: UNNotificationResponse,
+ withCompletionHandler completionHandler: @escaping () -> Void
+ ) {
+ var payload = Self._stringPayload(response.notification.request.content.userInfo)
+ payload["actionIdentifier"] = response.actionIdentifier
+ flutterApi?.onPushClicked(payload: payload) { _ in }
+ completionHandler()
+ }
+
+ public func userNotificationCenter(
+ _ center: UNUserNotificationCenter,
+ willPresent notification: UNNotification,
+ withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void
+ ) {
+ flutterApi?.onPushDelivered(payload: Self._stringPayload(notification.request.content.userInfo)) { _ in }
+ if #available(iOS 14.0, *) {
+ completionHandler([.badge, .sound, .banner, .list])
+ } else {
+ completionHandler([.badge, .sound, .alert])
+ }
+ }
+}
+
+extension Rees46FlutterSdkPlugin {
+ fileprivate static func _stringPayload(_ userInfo: [AnyHashable: Any]) -> [String: String?] {
+ var result: [String: String?] = [:]
+ for (keyAny, value) in userInfo {
+ let key = String(describing: keyAny)
+ if let str = value as? String {
+ result[key] = str
+ } else if JSONSerialization.isValidJSONObject(value),
+ let data = try? JSONSerialization.data(withJSONObject: value, options: []),
+ let json = String(data: data, encoding: .utf8) {
+ result[key] = json
+ } else {
+ result[key] = String(describing: value)
+ }
+ }
+ return result
+ }
+}
+
+final class PersonalizationHostApiImpl: PersonalizationHostApi {
+ func getStoredPushToken() throws -> String? {
+ guard let deviceToken = UserDefaults.standard.data(forKey: Rees46FlutterSdkPlugin.pushTokenKey) else {
+ return nil
+ }
+ let token = deviceToken.map { String(format: "%02.2hhx", $0) }.joined()
+ return token.isEmpty ? nil : token
+ }
+
+ func initialize(config: InitConfig, completion: @escaping (Result) -> Void) {
+ if config.shopId.isEmpty {
+ completion(.failure(PigeonError(code: "bad_args", message: "shopId is required", details: nil)))
+ return
+ }
+
+ let sdk = createPersonalizationSDK(
+ shopId: config.shopId,
+ apiDomain: config.apiDomain,
+ stream: config.stream,
+ enableLogs: config.enableLogs,
+ autoSendPushToken: config.autoSendPushToken,
+ sendAdvertisingId: config.sendAdvertisingId,
+ parentViewController: nil,
+ enableAutoPopupPresentation: config.enableAutoPopupPresentation,
+ needReInitialization: config.needReInitialization
+ ) { error in
+ if let error = error {
+ completion(.failure(PigeonError(code: "init_failed", message: String(describing: error), details: nil)))
+ } else {
+ completion(.success(()))
+ }
+ }
+
+ Rees46FlutterSdkPlugin.sdk = sdk
+
+ // Create notification service to receive AppDelegate callbacks (device token, remote notification).
+ let logger = NotificationLogger()
+ Rees46FlutterSdkPlugin.notificationService = NotificationService(
+ sdk: sdk,
+ notificationLogger: logger
+ )
+ }
+
+ func getPlatformVersion() throws -> String {
+ return "iOS " + UIDevice.current.systemVersion
+ }
+
+ func getRecommendation(code: String, paramsJson: String?, completion: @escaping (Result) -> Void) {
+ guard let sdk = Rees46FlutterSdkPlugin.sdk else {
+ completion(.failure(PigeonError(code: "not_initialized", message: "SDK is not initialized", details: nil)))
+ return
+ }
+ if code.isEmpty {
+ completion(.failure(PigeonError(code: "bad_args", message: "code is required", details: nil)))
+ return
+ }
+ let p = parseJsonObject(paramsJson)
+ let itemId = p?["item_id"] as? String
+ let categoryId = p?["category_id"] as? String
+ let locations = p?["locations"] as? String
+ let imageSize = (p?["image_size"] as? Int).map { String($0) }
+ let withLocations = p?["with_locations"] as? Bool ?? false
+
+ sdk.recommend(
+ blockId: code,
+ currentProductId: itemId,
+ currentCategoryId: categoryId,
+ locations: locations,
+ imageSize: imageSize,
+ timeOut: nil,
+ withLocations: withLocations,
+ extended: false
+ ) { result in
+ switch result {
+ case .success(let response):
+ let dict = Self._recommendationToDict(response)
+ guard let data = try? JSONSerialization.data(withJSONObject: dict),
+ let json = String(data: data, encoding: .utf8) else {
+ completion(.failure(PigeonError(code: "serialization_failed", message: "Failed to serialize recommendation response", details: nil)))
+ return
+ }
+ completion(.success(json))
+ case .failure(let err):
+ completion(.failure(PigeonError(code: "recommendation_failed", message: String(describing: err), details: nil)))
+ }
+ }
+ }
+
+ private static func _recommendationToDict(_ response: RecommenderResponse) -> [String: Any] {
+ return [
+ "title": response.title,
+ "recommends": response.recommended.map { _productToDict($0) },
+ ]
+ }
+
+ private static func _productToDict(_ p: Recommended) -> [String: Any] {
+ var dict: [String: Any] = [
+ "id": p.id,
+ "name": p.name,
+ "brand": p.brand,
+ "description": p.description,
+ "image_url": p.imageUrl,
+ "picture": p.resizedImageUrl,
+ "image_url_resized": p.resizedImages,
+ "url": p.url,
+ "price": p.price,
+ "price_full": p.priceFull,
+ "currency": p.currency,
+ "sales_rate": p.salesRate,
+ "relative_sales_rate": p.relativeSalesRate,
+ "categories": p.categories.map { _categoryToDict($0) },
+ ]
+ if let pf = p.priceFormatted { dict["price_formatted"] = pf }
+ if let pff = p.priceFullFormatted { dict["price_full_formatted"] = pff }
+ return dict
+ }
+
+ private static func _categoryToDict(_ c: Category) -> [String: Any] {
+ var dict: [String: Any] = ["id": c.id, "name": c.name]
+ if let parentId = c.parentId { dict["parent_id"] = parentId }
+ if let url = c.url { dict["url"] = url }
+ return dict
+ }
+
+ func getProductInfo(itemId: String, completion: @escaping (Result) -> Void) {
+ guard let sdk = Rees46FlutterSdkPlugin.sdk else {
+ completion(.failure(PigeonError(code: "not_initialized", message: "SDK is not initialized", details: nil)))
+ return
+ }
+ if itemId.isEmpty {
+ completion(.failure(PigeonError(code: "bad_args", message: "itemId is required", details: nil)))
+ return
+ }
+ sdk.getProductInfo(id: itemId) { result in
+ switch result {
+ case .success(let product):
+ let dict = Self._productInfoToDict(product)
+ guard let data = try? JSONSerialization.data(withJSONObject: dict),
+ let json = String(data: data, encoding: .utf8) else {
+ completion(.failure(PigeonError(code: "serialization_failed", message: "Failed to serialize product info", details: nil)))
+ return
+ }
+ completion(.success(json))
+ case .failure(let err):
+ completion(.failure(PigeonError(code: "product_info_failed", message: String(describing: err), details: nil)))
+ }
+ }
+ }
+
+ func getProductsList(paramsJson: String?, completion: @escaping (Result) -> Void) {
+ guard let sdk = Rees46FlutterSdkPlugin.sdk else {
+ completion(.failure(PigeonError(code: "not_initialized", message: "SDK is not initialized", details: nil)))
+ return
+ }
+ let p = parseJsonObject(paramsJson)
+ let brands = p?["brands"] as? String
+ let merchants = p?["merchants"] as? String
+ let categories = p?["categories"] as? String
+ let locations = p?["locations"] as? String
+ let limit = p?["limit"] as? Int
+ let page = p?["page"] as? Int
+ let filters = p?["filters"] as? [String: Any]
+
+ sdk.getProductsList(
+ brands: brands,
+ merchants: merchants,
+ categories: categories,
+ locations: locations,
+ limit: limit,
+ page: page,
+ filters: filters
+ ) { result in
+ switch result {
+ case .success(let response):
+ let dict = Self._productsListResponseToDict(response)
+ guard let data = try? JSONSerialization.data(withJSONObject: dict),
+ let json = String(data: data, encoding: .utf8) else {
+ completion(.failure(PigeonError(code: "serialization_failed", message: "Failed to serialize products list response", details: nil)))
+ return
+ }
+ completion(.success(json))
+ case .failure(let err):
+ completion(.failure(PigeonError(code: "products_list_failed", message: String(describing: err), details: nil)))
+ }
+ }
+ }
+
+ private static func _productsListResponseToDict(_ r: ProductsListResponse) -> [String: Any] {
+ var dict: [String: Any] = [
+ "products": r.products.map { _productInfoToDict($0) },
+ "products_total": r.productsTotal,
+ ]
+ if let pr = r.priceRange {
+ dict["price_range"] = ["min": pr.min, "max": pr.max]
+ }
+ return dict
+ }
+
+ private static func _productInfoToDict(_ p: ProductInfo) -> [String: Any] {
+ return [
+ "id": p.id, // normalised from "uniqid"
+ "name": p.name,
+ "brand": p.brand,
+ "description": p.description,
+ "image_url": p.imageUrl,
+ "image_url_resized": p.resizedImages,
+ "url": p.url,
+ "price": p.price,
+ "price_full": p.priceFull,
+ "price_formatted": p.priceFormatted,
+ "price_full_formatted": p.priceFullFormatted,
+ "currency": p.currency,
+ "sales_rate": p.salesRate,
+ "relative_sales_rate": p.relativeSalesRate,
+ "categories": p.categories.map { c -> [String: Any] in
+ var d: [String: Any] = ["id": c.id, "name": c.name]
+ if let parent = c.parentId { d["parent_id"] = parent }
+ if let url = c.url { d["url"] = url }
+ return d
+ },
+ ]
+ }
+
+ func searchBlank(completion: @escaping (Result) -> Void) {
+ guard let sdk = Rees46FlutterSdkPlugin.sdk else {
+ completion(.failure(PigeonError(code: "not_initialized", message: "SDK is not initialized", details: nil)))
+ return
+ }
+ sdk.searchBlank { result in
+ switch result {
+ case .success(let response):
+ let dict = Self._searchBlankResponseToDict(response)
+ guard let data = try? JSONSerialization.data(withJSONObject: dict),
+ let json = String(data: data, encoding: .utf8) else {
+ completion(.failure(PigeonError(code: "serialization_failed", message: "Failed to serialize search blank response", details: nil)))
+ return
+ }
+ completion(.success(json))
+ case .failure(let err):
+ completion(.failure(PigeonError(code: "search_blank_failed", message: String(describing: err), details: nil)))
+ }
+ }
+ }
+
+ private static func _searchBlankResponseToDict(_ r: SearchBlankResponse) -> [String: Any] {
+ return [
+ "products": r.products.map { _searchProductToDict($0) },
+ "suggests": r.suggests.map { ["name": $0.name, "url": $0.url] },
+ ]
+ }
+
+ func searchInstant(query: String, paramsJson: String?, completion: @escaping (Result) -> Void) {
+ guard let sdk = Rees46FlutterSdkPlugin.sdk else {
+ completion(.failure(PigeonError(code: "not_initialized", message: "SDK is not initialized", details: nil)))
+ return
+ }
+ if query.isEmpty {
+ completion(.failure(PigeonError(code: "bad_args", message: "query is required", details: nil)))
+ return
+ }
+ let p = parseJsonObject(paramsJson)
+ let locations = p?["locations"] as? String
+ let excludedBrands = p?["excluded_brands"] as? [String]
+
+ sdk.search(
+ query: query,
+ limit: nil,
+ offset: nil,
+ categoryLimit: nil,
+ brandLimit: nil,
+ categories: nil,
+ extended: nil,
+ sortBy: nil,
+ sortDir: nil,
+ locations: locations,
+ excludedMerchants: nil,
+ excludedBrands: excludedBrands,
+ brands: nil,
+ filters: nil,
+ priceMin: nil,
+ priceMax: nil,
+ colors: nil,
+ fashionSizes: nil,
+ exclude: nil,
+ email: nil,
+ timeOut: nil,
+ disableClarification: nil
+ ) { result in
+ switch result {
+ case .success(let response):
+ let dict = Self._searchResponseToDict(response)
+ guard let data = try? JSONSerialization.data(withJSONObject: dict),
+ let json = String(data: data, encoding: .utf8) else {
+ completion(.failure(PigeonError(code: "serialization_failed", message: "Failed to serialize search instant response", details: nil)))
+ return
+ }
+ completion(.success(json))
+ case .failure(let err):
+ completion(.failure(PigeonError(code: "search_instant_failed", message: String(describing: err), details: nil)))
+ }
+ }
+ }
+
+ func searchFull(query: String, paramsJson: String?, completion: @escaping (Result) -> Void) {
+ guard let sdk = Rees46FlutterSdkPlugin.sdk else {
+ completion(.failure(PigeonError(code: "not_initialized", message: "SDK is not initialized", details: nil)))
+ return
+ }
+ if query.isEmpty {
+ completion(.failure(PigeonError(code: "bad_args", message: "query is required", details: nil)))
+ return
+ }
+ let p = parseJsonObject(paramsJson)
+ let limit = p?["limit"] as? Int
+ let page = p?["page"] as? Int
+ let categoryLimit = p?["category_limit"] as? Int
+ let brandLimit = p?["brand_limit"] as? Int
+ let categoriesInt = (p?["categories"] as? [String])?.compactMap { Int($0) }
+ let sortBy = p?["sort_by"] as? String
+ let sortDir = p?["sort_dir"] as? String
+ let locations = p?["locations"] as? String
+ let excludedBrands = p?["excluded_brands"] as? [String]
+ let brands = p?["brands"] as? String
+ let priceMin = p?["price_min"] as? Double
+ let priceMax = p?["price_max"] as? Double
+ let colors = p?["colors"] as? [String]
+ let fashionSizes = p?["fashion_sizes"] as? [String]
+
+ sdk.search(
+ query: query,
+ limit: limit,
+ offset: page,
+ categoryLimit: categoryLimit,
+ brandLimit: brandLimit,
+ categories: categoriesInt,
+ extended: nil,
+ sortBy: sortBy,
+ sortDir: sortDir,
+ locations: locations,
+ excludedMerchants: nil,
+ excludedBrands: excludedBrands,
+ brands: brands,
+ filters: nil,
+ priceMin: priceMin,
+ priceMax: priceMax,
+ colors: colors,
+ fashionSizes: fashionSizes,
+ exclude: nil,
+ email: nil,
+ timeOut: nil,
+ disableClarification: nil
+ ) { result in
+ switch result {
+ case .success(let response):
+ let dict = Self._searchResponseToDict(response)
+ guard let data = try? JSONSerialization.data(withJSONObject: dict),
+ let json = String(data: data, encoding: .utf8) else {
+ completion(.failure(PigeonError(code: "serialization_failed", message: "Failed to serialize search response", details: nil)))
+ return
+ }
+ completion(.success(json))
+ case .failure(let err):
+ completion(.failure(PigeonError(code: "search_failed", message: String(describing: err), details: nil)))
+ }
+ }
+ }
+
+ private static func _searchResponseToDict(_ r: SearchResponse) -> [String: Any] {
+ var dict: [String: Any] = [
+ "products": r.products.map { _searchProductToDict($0) },
+ "categories": r.categories.map { _searchCategoryToDict($0) },
+ "products_total": r.productsTotal,
+ ]
+ if let pr = r.priceRange {
+ dict["price_range"] = ["min": pr.min, "max": pr.max]
+ }
+ if let locs = r.locations {
+ dict["locations"] = locs.map { loc -> [String: Any] in
+ var d: [String: Any] = ["id": loc.id, "name": loc.name]
+ if let t = loc.type { d["type"] = t }
+ return d
+ }
+ }
+ return dict
+ }
+
+ private static func _searchProductToDict(_ p: Product) -> [String: Any] {
+ var dict: [String: Any] = [
+ "id": p.id,
+ "name": p.name,
+ "brand": p.brand,
+ "description": p.description,
+ "image_url": p.imageUrl,
+ "picture": p.resizedImageUrl,
+ "image_url_resized": p.resizedImages,
+ "url": p.url,
+ "price": p.price,
+ "price_full": p.priceFull,
+ "price_formatted": p.priceFormatted,
+ "price_full_formatted": p.priceFullFormatted,
+ "currency": p.currency,
+ "sales_rate": p.salesRate,
+ "relative_sales_rate": p.relativeSalesRate,
+ ]
+ _ = dict // suppress unused warning; all fields set above
+ return dict
+ }
+
+ private static func _searchCategoryToDict(_ c: Category) -> [String: Any] {
+ var dict: [String: Any] = ["id": c.id, "name": c.name]
+ if let url = c.url { dict["url"] = url }
+ if let parent = c.parentId { dict["parent"] = parent }
+ if let count = c.count { dict["count"] = count }
+ return dict
+ }
+
+ func getSid() throws -> String {
+ guard let sdk = Rees46FlutterSdkPlugin.sdk else {
+ throw PigeonError(code: "not_initialized", message: "SDK is not initialized", details: nil)
+ }
+ return sdk.userSeance
+ }
+
+ func getDid() throws -> String? {
+ return Rees46FlutterSdkPlugin.sdk?.deviceId
+ }
+
+ func setProfile(params: ProfileParamsWire, completion: @escaping (Result) -> Void) {
+ guard let sdk = Rees46FlutterSdkPlugin.sdk else {
+ completion(.failure(PigeonError(code: "not_initialized", message: "SDK is not initialized", details: nil)))
+ return
+ }
+ var profileData = ProfileData()
+ profileData.userEmail = params.email
+ profileData.userPhone = params.phone
+ profileData.userLoyaltyId = params.loyaltyId
+ profileData.firstName = params.firstName
+ profileData.lastName = params.lastName
+ profileData.age = params.age.map { Int($0) }
+ profileData.location = params.location
+ profileData.advertisingId = params.advertisingId
+ profileData.fbID = params.fbId
+ profileData.vkID = params.vkId
+ profileData.telegramId = params.telegramId
+ profileData.loyaltyCardLocation = params.loyaltyCardLocation
+ profileData.loyaltyStatus = params.loyaltyStatus
+ profileData.loyaltyBonuses = params.loyaltyBonuses.map { Int($0) }
+ profileData.loyaltyBonusesToNextLevel = params.loyaltyBonusesToNextLevel.map { Int($0) }
+ profileData.boughtSomething = params.boughtSomething
+ profileData.userId = params.userId
+ if let genderStr = params.gender {
+ profileData.gender = genderStr == "m" ? .male : .female
+ }
+ if let birthdayStr = params.birthday {
+ let fmt = DateFormatter()
+ fmt.dateFormat = "yyyy-MM-dd"
+ profileData.birthday = fmt.date(from: birthdayStr)
+ }
+ if let json = params.customPropertiesJson,
+ let data = json.data(using: .utf8),
+ let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
+ profileData.customProperties = obj
+ }
+ sdk.setProfileData(profileData: profileData) { result in
+ switch result {
+ case .success:
+ completion(.success(()))
+ case .failure(let err):
+ completion(.failure(PigeonError(code: "set_profile_failed", message: String(describing: err), details: nil)))
+ }
+ }
+ }
+
+ func trackEvent(
+ event: String,
+ time: Int64?,
+ category: String?,
+ label: String?,
+ value: Int64?,
+ customFieldsJson: String?,
+ completion: @escaping (Result) -> Void
+ ) {
+ guard let sdk = Rees46FlutterSdkPlugin.sdk else {
+ completion(
+ .failure(
+ PigeonError(
+ code: "not_initialized",
+ message: "SDK is not initialized",
+ details: nil)))
+ return
+ }
+ if event.isEmpty {
+ completion(
+ .failure(PigeonError(code: "bad_args", message: "event is required", details: nil)))
+ return
+ }
+ let timeInt: Int? = time.map { Int(truncatingIfNeeded: $0) }
+ let valueInt: Int? = value.map { Int(truncatingIfNeeded: $0) }
+ let customFields = parseJsonObject(customFieldsJson)
+ sdk.trackEvent(
+ event: event,
+ time: timeInt,
+ category: category,
+ label: label,
+ value: valueInt,
+ customFields: customFields
+ ) { result in
+ switch result {
+ case .success:
+ completion(.success(()))
+ case .failure(let err):
+ completion(
+ .failure(
+ PigeonError(
+ code: "track_event_failed",
+ message: String(describing: err),
+ details: nil)))
+ }
+ }
+ }
+
+ func trackPurchase(
+ orderId: String,
+ orderPrice: Double,
+ items: [PurchaseLineItemWire],
+ deliveryType: String?,
+ deliveryAddress: String?,
+ paymentType: String?,
+ isTaxFree: Bool,
+ promocode: String?,
+ orderCash: Double?,
+ orderBonuses: Double?,
+ orderDelivery: Double?,
+ orderDiscount: Double?,
+ channel: String?,
+ customJson: String?,
+ recommendedSourceJson: String?,
+ stream: String?,
+ segment: String?,
+ completion: @escaping (Result) -> Void
+ ) {
+ guard let sdk = Rees46FlutterSdkPlugin.sdk else {
+ completion(
+ .failure(
+ PigeonError(
+ code: "not_initialized",
+ message: "SDK is not initialized",
+ details: nil)))
+ return
+ }
+ if orderId.isEmpty {
+ completion(
+ .failure(PigeonError(code: "bad_args", message: "orderId is required", details: nil)))
+ return
+ }
+ if items.isEmpty {
+ completion(
+ .failure(PigeonError(code: "bad_args", message: "items must be non-empty", details: nil)))
+ return
+ }
+ let itemRequests: [PurchaseItemRequest] = items.map { wire in
+ PurchaseItemRequest(
+ id: wire.id,
+ amount: Int(wire.amount),
+ price: wire.price,
+ quantity: nil,
+ lineId: wire.lineId,
+ fashionSize: wire.fashionSize
+ )
+ }
+ let request = PurchaseTrackingRequest(
+ orderId: orderId,
+ orderPrice: orderPrice,
+ items: itemRequests,
+ deliveryType: deliveryType,
+ deliveryAddress: deliveryAddress,
+ paymentType: paymentType,
+ isTaxFree: isTaxFree,
+ promocode: promocode,
+ orderCash: orderCash,
+ orderBonuses: orderBonuses,
+ orderDelivery: orderDelivery,
+ orderDiscount: orderDiscount,
+ channel: channel,
+ custom: parseJsonObject(customJson),
+ recommendedSource: parseJsonObject(recommendedSourceJson),
+ stream: stream,
+ segment: segment
+ )
+ sdk.trackPurchase(request, recommendedBy: nil) { result in
+ switch result {
+ case .success:
+ completion(.success(()))
+ case .failure(let err):
+ completion(
+ .failure(
+ PigeonError(
+ code: "track_purchase_failed",
+ message: String(describing: err),
+ details: nil)))
+ }
+ }
+ }
+
+ private func parseJsonObject(_ json: String?) -> [String: Any]? {
+ guard let json, !json.isEmpty, let data = json.data(using: .utf8) else { return nil }
+ let obj = try? JSONSerialization.jsonObject(with: data)
+ return obj as? [String: Any]
+ }
+}
diff --git a/ios/Classes/pigeon/PersonalizationApi.g.swift b/ios/Classes/pigeon/PersonalizationApi.g.swift
new file mode 100644
index 0000000..76222a7
--- /dev/null
+++ b/ios/Classes/pigeon/PersonalizationApi.g.swift
@@ -0,0 +1,769 @@
+// Autogenerated from Pigeon (v25.5.0), do not edit directly.
+// See also: https://pub.dev/packages/pigeon
+
+import Foundation
+
+#if os(iOS)
+ import Flutter
+#elseif os(macOS)
+ import FlutterMacOS
+#else
+ #error("Unsupported platform.")
+#endif
+
+/// Error class for passing custom error details to Dart side.
+final class PigeonError: Error {
+ let code: String
+ let message: String?
+ let details: Sendable?
+
+ init(code: String, message: String?, details: Sendable?) {
+ self.code = code
+ self.message = message
+ self.details = details
+ }
+
+ var localizedDescription: String {
+ return
+ "PigeonError(code: \(code), message: \(message ?? ""), details: \(details ?? "")"
+ }
+}
+
+private func wrapResult(_ result: Any?) -> [Any?] {
+ return [result]
+}
+
+private func wrapError(_ error: Any) -> [Any?] {
+ if let pigeonError = error as? PigeonError {
+ return [
+ pigeonError.code,
+ pigeonError.message,
+ pigeonError.details,
+ ]
+ }
+ if let flutterError = error as? FlutterError {
+ return [
+ flutterError.code,
+ flutterError.message,
+ flutterError.details,
+ ]
+ }
+ return [
+ "\(error)",
+ "\(type(of: error))",
+ "Stacktrace: \(Thread.callStackSymbols)",
+ ]
+}
+
+private func createConnectionError(withChannelName channelName: String) -> PigeonError {
+ return PigeonError(code: "channel-error", message: "Unable to establish connection on channel: '\(channelName)'.", details: "")
+}
+
+private func isNullish(_ value: Any?) -> Bool {
+ return value is NSNull || value == nil
+}
+
+private func nilOrValue(_ value: Any?) -> T? {
+ if value is NSNull { return nil }
+ return value as! T?
+}
+
+func deepEqualsPersonalizationApi(_ lhs: Any?, _ rhs: Any?) -> Bool {
+ let cleanLhs = nilOrValue(lhs) as Any?
+ let cleanRhs = nilOrValue(rhs) as Any?
+ switch (cleanLhs, cleanRhs) {
+ case (nil, nil):
+ return true
+
+ case (nil, _), (_, nil):
+ return false
+
+ case is (Void, Void):
+ return true
+
+ case let (cleanLhsHashable, cleanRhsHashable) as (AnyHashable, AnyHashable):
+ return cleanLhsHashable == cleanRhsHashable
+
+ case let (cleanLhsArray, cleanRhsArray) as ([Any?], [Any?]):
+ guard cleanLhsArray.count == cleanRhsArray.count else { return false }
+ for (index, element) in cleanLhsArray.enumerated() {
+ if !deepEqualsPersonalizationApi(element, cleanRhsArray[index]) {
+ return false
+ }
+ }
+ return true
+
+ case let (cleanLhsDictionary, cleanRhsDictionary) as ([AnyHashable: Any?], [AnyHashable: Any?]):
+ guard cleanLhsDictionary.count == cleanRhsDictionary.count else { return false }
+ for (key, cleanLhsValue) in cleanLhsDictionary {
+ guard cleanRhsDictionary.index(forKey: key) != nil else { return false }
+ if !deepEqualsPersonalizationApi(cleanLhsValue, cleanRhsDictionary[key]!) {
+ return false
+ }
+ }
+ return true
+
+ default:
+ // Any other type shouldn't be able to be used with pigeon. File an issue if you find this to be untrue.
+ return false
+ }
+}
+
+func deepHashPersonalizationApi(value: Any?, hasher: inout Hasher) {
+ if let valueList = value as? [AnyHashable] {
+ for item in valueList { deepHashPersonalizationApi(value: item, hasher: &hasher) }
+ return
+ }
+
+ if let valueDict = value as? [AnyHashable: AnyHashable] {
+ for key in valueDict.keys {
+ hasher.combine(key)
+ deepHashPersonalizationApi(value: valueDict[key]!, hasher: &hasher)
+ }
+ return
+ }
+
+ if let hashableValue = value as? AnyHashable {
+ hasher.combine(hashableValue.hashValue)
+ }
+
+ return hasher.combine(String(describing: value))
+}
+
+
+
+/// Generated class from Pigeon that represents data sent in messages.
+struct InitConfig: Hashable {
+ var shopId: String
+ var apiDomain: String
+ var stream: String
+ var enableLogs: Bool
+ var autoSendPushToken: Bool
+ var sendAdvertisingId: Bool
+ var enableAutoPopupPresentation: Bool
+ var needReInitialization: Bool
+
+
+ // swift-format-ignore: AlwaysUseLowerCamelCase
+ static func fromList(_ pigeonVar_list: [Any?]) -> InitConfig? {
+ let shopId = pigeonVar_list[0] as! String
+ let apiDomain = pigeonVar_list[1] as! String
+ let stream = pigeonVar_list[2] as! String
+ let enableLogs = pigeonVar_list[3] as! Bool
+ let autoSendPushToken = pigeonVar_list[4] as! Bool
+ let sendAdvertisingId = pigeonVar_list[5] as! Bool
+ let enableAutoPopupPresentation = pigeonVar_list[6] as! Bool
+ let needReInitialization = pigeonVar_list[7] as! Bool
+
+ return InitConfig(
+ shopId: shopId,
+ apiDomain: apiDomain,
+ stream: stream,
+ enableLogs: enableLogs,
+ autoSendPushToken: autoSendPushToken,
+ sendAdvertisingId: sendAdvertisingId,
+ enableAutoPopupPresentation: enableAutoPopupPresentation,
+ needReInitialization: needReInitialization
+ )
+ }
+ func toList() -> [Any?] {
+ return [
+ shopId,
+ apiDomain,
+ stream,
+ enableLogs,
+ autoSendPushToken,
+ sendAdvertisingId,
+ enableAutoPopupPresentation,
+ needReInitialization,
+ ]
+ }
+ static func == (lhs: InitConfig, rhs: InitConfig) -> Bool {
+ return deepEqualsPersonalizationApi(lhs.toList(), rhs.toList()) }
+ func hash(into hasher: inout Hasher) {
+ deepHashPersonalizationApi(value: toList(), hasher: &hasher)
+ }
+}
+
+/// Wire format for one purchase line (maps to native [PurchaseItemRequest]; quantity not exposed).
+///
+/// Generated class from Pigeon that represents data sent in messages.
+struct PurchaseLineItemWire: Hashable {
+ var id: String
+ var amount: Int64
+ var price: Double
+ var lineId: String? = nil
+ var fashionSize: String? = nil
+
+
+ // swift-format-ignore: AlwaysUseLowerCamelCase
+ static func fromList(_ pigeonVar_list: [Any?]) -> PurchaseLineItemWire? {
+ let id = pigeonVar_list[0] as! String
+ let amount = pigeonVar_list[1] as! Int64
+ let price = pigeonVar_list[2] as! Double
+ let lineId: String? = nilOrValue(pigeonVar_list[3])
+ let fashionSize: String? = nilOrValue(pigeonVar_list[4])
+
+ return PurchaseLineItemWire(
+ id: id,
+ amount: amount,
+ price: price,
+ lineId: lineId,
+ fashionSize: fashionSize
+ )
+ }
+ func toList() -> [Any?] {
+ return [
+ id,
+ amount,
+ price,
+ lineId,
+ fashionSize,
+ ]
+ }
+ static func == (lhs: PurchaseLineItemWire, rhs: PurchaseLineItemWire) -> Bool {
+ return deepEqualsPersonalizationApi(lhs.toList(), rhs.toList()) }
+ func hash(into hasher: inout Hasher) {
+ deepHashPersonalizationApi(value: toList(), hasher: &hasher)
+ }
+}
+
+/// Wire format for profile fields sent to native SDK.
+/// All fields are optional — only non-null values are forwarded.
+/// [birthday] must be a "yyyy-MM-dd" string.
+/// [gender] must be "m" or "f".
+/// [customPropertiesJson] is a JSON object string or null.
+///
+/// Generated class from Pigeon that represents data sent in messages.
+struct ProfileParamsWire: Hashable {
+ var email: String? = nil
+ var phone: String? = nil
+ var loyaltyId: String? = nil
+ var firstName: String? = nil
+ var lastName: String? = nil
+ var birthday: String? = nil
+ var age: Int64? = nil
+ var gender: String? = nil
+ var location: String? = nil
+ var advertisingId: String? = nil
+ var fbId: String? = nil
+ var vkId: String? = nil
+ var telegramId: String? = nil
+ var loyaltyCardLocation: String? = nil
+ var loyaltyStatus: String? = nil
+ var loyaltyBonuses: Int64? = nil
+ var loyaltyBonusesToNextLevel: Int64? = nil
+ var boughtSomething: Bool? = nil
+ var userId: String? = nil
+ var customPropertiesJson: String? = nil
+
+
+ // swift-format-ignore: AlwaysUseLowerCamelCase
+ static func fromList(_ pigeonVar_list: [Any?]) -> ProfileParamsWire? {
+ let email: String? = nilOrValue(pigeonVar_list[0])
+ let phone: String? = nilOrValue(pigeonVar_list[1])
+ let loyaltyId: String? = nilOrValue(pigeonVar_list[2])
+ let firstName: String? = nilOrValue(pigeonVar_list[3])
+ let lastName: String? = nilOrValue(pigeonVar_list[4])
+ let birthday: String? = nilOrValue(pigeonVar_list[5])
+ let age: Int64? = nilOrValue(pigeonVar_list[6])
+ let gender: String? = nilOrValue(pigeonVar_list[7])
+ let location: String? = nilOrValue(pigeonVar_list[8])
+ let advertisingId: String? = nilOrValue(pigeonVar_list[9])
+ let fbId: String? = nilOrValue(pigeonVar_list[10])
+ let vkId: String? = nilOrValue(pigeonVar_list[11])
+ let telegramId: String? = nilOrValue(pigeonVar_list[12])
+ let loyaltyCardLocation: String? = nilOrValue(pigeonVar_list[13])
+ let loyaltyStatus: String? = nilOrValue(pigeonVar_list[14])
+ let loyaltyBonuses: Int64? = nilOrValue(pigeonVar_list[15])
+ let loyaltyBonusesToNextLevel: Int64? = nilOrValue(pigeonVar_list[16])
+ let boughtSomething: Bool? = nilOrValue(pigeonVar_list[17])
+ let userId: String? = nilOrValue(pigeonVar_list[18])
+ let customPropertiesJson: String? = nilOrValue(pigeonVar_list[19])
+
+ return ProfileParamsWire(
+ email: email,
+ phone: phone,
+ loyaltyId: loyaltyId,
+ firstName: firstName,
+ lastName: lastName,
+ birthday: birthday,
+ age: age,
+ gender: gender,
+ location: location,
+ advertisingId: advertisingId,
+ fbId: fbId,
+ vkId: vkId,
+ telegramId: telegramId,
+ loyaltyCardLocation: loyaltyCardLocation,
+ loyaltyStatus: loyaltyStatus,
+ loyaltyBonuses: loyaltyBonuses,
+ loyaltyBonusesToNextLevel: loyaltyBonusesToNextLevel,
+ boughtSomething: boughtSomething,
+ userId: userId,
+ customPropertiesJson: customPropertiesJson
+ )
+ }
+ func toList() -> [Any?] {
+ return [
+ email,
+ phone,
+ loyaltyId,
+ firstName,
+ lastName,
+ birthday,
+ age,
+ gender,
+ location,
+ advertisingId,
+ fbId,
+ vkId,
+ telegramId,
+ loyaltyCardLocation,
+ loyaltyStatus,
+ loyaltyBonuses,
+ loyaltyBonusesToNextLevel,
+ boughtSomething,
+ userId,
+ customPropertiesJson,
+ ]
+ }
+ static func == (lhs: ProfileParamsWire, rhs: ProfileParamsWire) -> Bool {
+ return deepEqualsPersonalizationApi(lhs.toList(), rhs.toList()) }
+ func hash(into hasher: inout Hasher) {
+ deepHashPersonalizationApi(value: toList(), hasher: &hasher)
+ }
+}
+
+private class PersonalizationApiPigeonCodecReader: FlutterStandardReader {
+ override func readValue(ofType type: UInt8) -> Any? {
+ switch type {
+ case 129:
+ return InitConfig.fromList(self.readValue() as! [Any?])
+ case 130:
+ return PurchaseLineItemWire.fromList(self.readValue() as! [Any?])
+ case 131:
+ return ProfileParamsWire.fromList(self.readValue() as! [Any?])
+ default:
+ return super.readValue(ofType: type)
+ }
+ }
+}
+
+private class PersonalizationApiPigeonCodecWriter: FlutterStandardWriter {
+ override func writeValue(_ value: Any) {
+ if let value = value as? InitConfig {
+ super.writeByte(129)
+ super.writeValue(value.toList())
+ } else if let value = value as? PurchaseLineItemWire {
+ super.writeByte(130)
+ super.writeValue(value.toList())
+ } else if let value = value as? ProfileParamsWire {
+ super.writeByte(131)
+ super.writeValue(value.toList())
+ } else {
+ super.writeValue(value)
+ }
+ }
+}
+
+private class PersonalizationApiPigeonCodecReaderWriter: FlutterStandardReaderWriter {
+ override func reader(with data: Data) -> FlutterStandardReader {
+ return PersonalizationApiPigeonCodecReader(data: data)
+ }
+
+ override func writer(with data: NSMutableData) -> FlutterStandardWriter {
+ return PersonalizationApiPigeonCodecWriter(data: data)
+ }
+}
+
+class PersonalizationApiPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable {
+ static let shared = PersonalizationApiPigeonCodec(readerWriter: PersonalizationApiPigeonCodecReaderWriter())
+}
+
+
+/// Generated protocol from Pigeon that represents a handler of messages from Flutter.
+protocol PersonalizationHostApi {
+ func initialize(config: InitConfig, completion: @escaping (Result) -> Void)
+ func getPlatformVersion() throws -> String
+ /// Returns the push token stored by the native SDK (if any).
+ func getStoredPushToken() throws -> String?
+ /// [customFieldsJson] is JSON object string or null (maps to native custom fields map).
+ func trackEvent(event: String, time: Int64?, category: String?, label: String?, value: Int64?, customFieldsJson: String?, completion: @escaping (Result) -> Void)
+ func setProfile(params: ProfileParamsWire, completion: @escaping (Result) -> Void)
+ /// Returns the recommendation block as a JSON string.
+ /// [paramsJson] is a JSON object string with optional filter parameters.
+ /// Dart layer parses the result into [RecommendationResponse].
+ func getRecommendation(code: String, paramsJson: String?, completion: @escaping (Result) -> Void)
+ /// Returns the current session ID from the native SDK.
+ func getSid() throws -> String
+ /// Returns the device ID assigned by the native SDK, or null before first sync.
+ func getDid() throws -> String?
+ /// Returns a single product's details as a JSON string.
+ /// Dart layer parses the result into [Product].
+ func getProductInfo(itemId: String, completion: @escaping (Result) -> Void)
+ /// Returns a paginated product catalog list as a JSON string.
+ /// [paramsJson] is a JSON object with optional filter fields.
+ /// Dart layer parses the result into [ProductsListResponse].
+ func getProductsList(paramsJson: String?, completion: @escaping (Result) -> Void)
+ /// Returns blank search results (trending/popular) as a JSON string.
+ /// No parameters — the native SDK decides what to return based on shop config.
+ /// Dart layer parses the result into [SearchBlankResponse].
+ func searchBlank(completion: @escaping (Result) -> Void)
+ /// Returns instant (typeahead) search results as a JSON string.
+ /// [paramsJson] may contain optional "locations" (String) and "excluded_brands" ([String]).
+ /// Dart layer parses the result into [SearchInstantResponse].
+ func searchInstant(query: String, paramsJson: String?, completion: @escaping (Result) -> Void)
+ /// Returns full search results as a JSON string.
+ /// [paramsJson] is a JSON object string with optional search parameters.
+ /// Dart layer parses the result into [SearchFullResponse].
+ func searchFull(query: String, paramsJson: String?, completion: @escaping (Result) -> Void)
+ /// [customJson] and [recommendedSourceJson] are JSON object strings or null.
+ func trackPurchase(orderId: String, orderPrice: Double, items: [PurchaseLineItemWire], deliveryType: String?, deliveryAddress: String?, paymentType: String?, isTaxFree: Bool, promocode: String?, orderCash: Double?, orderBonuses: Double?, orderDelivery: Double?, orderDiscount: Double?, channel: String?, customJson: String?, recommendedSourceJson: String?, stream: String?, segment: String?, completion: @escaping (Result) -> Void)
+}
+
+/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
+class PersonalizationHostApiSetup {
+ static var codec: FlutterStandardMessageCodec { PersonalizationApiPigeonCodec.shared }
+ /// Sets up an instance of `PersonalizationHostApi` to handle messages through the `binaryMessenger`.
+ static func setUp(binaryMessenger: FlutterBinaryMessenger, api: PersonalizationHostApi?, messageChannelSuffix: String = "") {
+ let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : ""
+ let initializeChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.personalization_flutter_sdk.PersonalizationHostApi.initialize\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
+ if let api = api {
+ initializeChannel.setMessageHandler { message, reply in
+ let args = message as! [Any?]
+ let configArg = args[0] as! InitConfig
+ api.initialize(config: configArg) { result in
+ switch result {
+ case .success:
+ reply(wrapResult(nil))
+ case .failure(let error):
+ reply(wrapError(error))
+ }
+ }
+ }
+ } else {
+ initializeChannel.setMessageHandler(nil)
+ }
+ let getPlatformVersionChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.personalization_flutter_sdk.PersonalizationHostApi.getPlatformVersion\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
+ if let api = api {
+ getPlatformVersionChannel.setMessageHandler { _, reply in
+ do {
+ let result = try api.getPlatformVersion()
+ reply(wrapResult(result))
+ } catch {
+ reply(wrapError(error))
+ }
+ }
+ } else {
+ getPlatformVersionChannel.setMessageHandler(nil)
+ }
+ /// Returns the push token stored by the native SDK (if any).
+ let getStoredPushTokenChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.personalization_flutter_sdk.PersonalizationHostApi.getStoredPushToken\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
+ if let api = api {
+ getStoredPushTokenChannel.setMessageHandler { _, reply in
+ do {
+ let result = try api.getStoredPushToken()
+ reply(wrapResult(result))
+ } catch {
+ reply(wrapError(error))
+ }
+ }
+ } else {
+ getStoredPushTokenChannel.setMessageHandler(nil)
+ }
+ /// [customFieldsJson] is JSON object string or null (maps to native custom fields map).
+ let trackEventChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.personalization_flutter_sdk.PersonalizationHostApi.trackEvent\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
+ if let api = api {
+ trackEventChannel.setMessageHandler { message, reply in
+ let args = message as! [Any?]
+ let eventArg = args[0] as! String
+ let timeArg: Int64? = nilOrValue(args[1])
+ let categoryArg: String? = nilOrValue(args[2])
+ let labelArg: String? = nilOrValue(args[3])
+ let valueArg: Int64? = nilOrValue(args[4])
+ let customFieldsJsonArg: String? = nilOrValue(args[5])
+ api.trackEvent(event: eventArg, time: timeArg, category: categoryArg, label: labelArg, value: valueArg, customFieldsJson: customFieldsJsonArg) { result in
+ switch result {
+ case .success:
+ reply(wrapResult(nil))
+ case .failure(let error):
+ reply(wrapError(error))
+ }
+ }
+ }
+ } else {
+ trackEventChannel.setMessageHandler(nil)
+ }
+ let setProfileChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.personalization_flutter_sdk.PersonalizationHostApi.setProfile\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
+ if let api = api {
+ setProfileChannel.setMessageHandler { message, reply in
+ let args = message as! [Any?]
+ let paramsArg = args[0] as! ProfileParamsWire
+ api.setProfile(params: paramsArg) { result in
+ switch result {
+ case .success:
+ reply(wrapResult(nil))
+ case .failure(let error):
+ reply(wrapError(error))
+ }
+ }
+ }
+ } else {
+ setProfileChannel.setMessageHandler(nil)
+ }
+ /// Returns the recommendation block as a JSON string.
+ /// [paramsJson] is a JSON object string with optional filter parameters.
+ /// Dart layer parses the result into [RecommendationResponse].
+ let getRecommendationChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.personalization_flutter_sdk.PersonalizationHostApi.getRecommendation\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
+ if let api = api {
+ getRecommendationChannel.setMessageHandler { message, reply in
+ let args = message as! [Any?]
+ let codeArg = args[0] as! String
+ let paramsJsonArg: String? = nilOrValue(args[1])
+ api.getRecommendation(code: codeArg, paramsJson: paramsJsonArg) { result in
+ switch result {
+ case .success(let res):
+ reply(wrapResult(res))
+ case .failure(let error):
+ reply(wrapError(error))
+ }
+ }
+ }
+ } else {
+ getRecommendationChannel.setMessageHandler(nil)
+ }
+ /// Returns the current session ID from the native SDK.
+ let getSidChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.personalization_flutter_sdk.PersonalizationHostApi.getSid\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
+ if let api = api {
+ getSidChannel.setMessageHandler { _, reply in
+ do {
+ let result = try api.getSid()
+ reply(wrapResult(result))
+ } catch {
+ reply(wrapError(error))
+ }
+ }
+ } else {
+ getSidChannel.setMessageHandler(nil)
+ }
+ /// Returns the device ID assigned by the native SDK, or null before first sync.
+ let getDidChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.personalization_flutter_sdk.PersonalizationHostApi.getDid\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
+ if let api = api {
+ getDidChannel.setMessageHandler { _, reply in
+ do {
+ let result = try api.getDid()
+ reply(wrapResult(result))
+ } catch {
+ reply(wrapError(error))
+ }
+ }
+ } else {
+ getDidChannel.setMessageHandler(nil)
+ }
+ /// Returns a single product's details as a JSON string.
+ /// Dart layer parses the result into [Product].
+ let getProductInfoChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.personalization_flutter_sdk.PersonalizationHostApi.getProductInfo\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
+ if let api = api {
+ getProductInfoChannel.setMessageHandler { message, reply in
+ let args = message as! [Any?]
+ let itemIdArg = args[0] as! String
+ api.getProductInfo(itemId: itemIdArg) { result in
+ switch result {
+ case .success(let res):
+ reply(wrapResult(res))
+ case .failure(let error):
+ reply(wrapError(error))
+ }
+ }
+ }
+ } else {
+ getProductInfoChannel.setMessageHandler(nil)
+ }
+ /// Returns a paginated product catalog list as a JSON string.
+ /// [paramsJson] is a JSON object with optional filter fields.
+ /// Dart layer parses the result into [ProductsListResponse].
+ let getProductsListChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.personalization_flutter_sdk.PersonalizationHostApi.getProductsList\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
+ if let api = api {
+ getProductsListChannel.setMessageHandler { message, reply in
+ let args = message as! [Any?]
+ let paramsJsonArg: String? = nilOrValue(args[0])
+ api.getProductsList(paramsJson: paramsJsonArg) { result in
+ switch result {
+ case .success(let res):
+ reply(wrapResult(res))
+ case .failure(let error):
+ reply(wrapError(error))
+ }
+ }
+ }
+ } else {
+ getProductsListChannel.setMessageHandler(nil)
+ }
+ /// Returns blank search results (trending/popular) as a JSON string.
+ /// No parameters — the native SDK decides what to return based on shop config.
+ /// Dart layer parses the result into [SearchBlankResponse].
+ let searchBlankChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.personalization_flutter_sdk.PersonalizationHostApi.searchBlank\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
+ if let api = api {
+ searchBlankChannel.setMessageHandler { _, reply in
+ api.searchBlank { result in
+ switch result {
+ case .success(let res):
+ reply(wrapResult(res))
+ case .failure(let error):
+ reply(wrapError(error))
+ }
+ }
+ }
+ } else {
+ searchBlankChannel.setMessageHandler(nil)
+ }
+ /// Returns instant (typeahead) search results as a JSON string.
+ /// [paramsJson] may contain optional "locations" (String) and "excluded_brands" ([String]).
+ /// Dart layer parses the result into [SearchInstantResponse].
+ let searchInstantChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.personalization_flutter_sdk.PersonalizationHostApi.searchInstant\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
+ if let api = api {
+ searchInstantChannel.setMessageHandler { message, reply in
+ let args = message as! [Any?]
+ let queryArg = args[0] as! String
+ let paramsJsonArg: String? = nilOrValue(args[1])
+ api.searchInstant(query: queryArg, paramsJson: paramsJsonArg) { result in
+ switch result {
+ case .success(let res):
+ reply(wrapResult(res))
+ case .failure(let error):
+ reply(wrapError(error))
+ }
+ }
+ }
+ } else {
+ searchInstantChannel.setMessageHandler(nil)
+ }
+ /// Returns full search results as a JSON string.
+ /// [paramsJson] is a JSON object string with optional search parameters.
+ /// Dart layer parses the result into [SearchFullResponse].
+ let searchFullChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.personalization_flutter_sdk.PersonalizationHostApi.searchFull\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
+ if let api = api {
+ searchFullChannel.setMessageHandler { message, reply in
+ let args = message as! [Any?]
+ let queryArg = args[0] as! String
+ let paramsJsonArg: String? = nilOrValue(args[1])
+ api.searchFull(query: queryArg, paramsJson: paramsJsonArg) { result in
+ switch result {
+ case .success(let res):
+ reply(wrapResult(res))
+ case .failure(let error):
+ reply(wrapError(error))
+ }
+ }
+ }
+ } else {
+ searchFullChannel.setMessageHandler(nil)
+ }
+ /// [customJson] and [recommendedSourceJson] are JSON object strings or null.
+ let trackPurchaseChannel = FlutterBasicMessageChannel(name: "dev.flutter.pigeon.personalization_flutter_sdk.PersonalizationHostApi.trackPurchase\(channelSuffix)", binaryMessenger: binaryMessenger, codec: codec)
+ if let api = api {
+ trackPurchaseChannel.setMessageHandler { message, reply in
+ let args = message as! [Any?]
+ let orderIdArg = args[0] as! String
+ let orderPriceArg = args[1] as! Double
+ let itemsArg = args[2] as! [PurchaseLineItemWire]
+ let deliveryTypeArg: String? = nilOrValue(args[3])
+ let deliveryAddressArg: String? = nilOrValue(args[4])
+ let paymentTypeArg: String? = nilOrValue(args[5])
+ let isTaxFreeArg = args[6] as! Bool
+ let promocodeArg: String? = nilOrValue(args[7])
+ let orderCashArg: Double? = nilOrValue(args[8])
+ let orderBonusesArg: Double? = nilOrValue(args[9])
+ let orderDeliveryArg: Double? = nilOrValue(args[10])
+ let orderDiscountArg: Double? = nilOrValue(args[11])
+ let channelArg: String? = nilOrValue(args[12])
+ let customJsonArg: String? = nilOrValue(args[13])
+ let recommendedSourceJsonArg: String? = nilOrValue(args[14])
+ let streamArg: String? = nilOrValue(args[15])
+ let segmentArg: String? = nilOrValue(args[16])
+ api.trackPurchase(orderId: orderIdArg, orderPrice: orderPriceArg, items: itemsArg, deliveryType: deliveryTypeArg, deliveryAddress: deliveryAddressArg, paymentType: paymentTypeArg, isTaxFree: isTaxFreeArg, promocode: promocodeArg, orderCash: orderCashArg, orderBonuses: orderBonusesArg, orderDelivery: orderDeliveryArg, orderDiscount: orderDiscountArg, channel: channelArg, customJson: customJsonArg, recommendedSourceJson: recommendedSourceJsonArg, stream: streamArg, segment: segmentArg) { result in
+ switch result {
+ case .success:
+ reply(wrapResult(nil))
+ case .failure(let error):
+ reply(wrapError(error))
+ }
+ }
+ }
+ } else {
+ trackPurchaseChannel.setMessageHandler(nil)
+ }
+ }
+}
+/// Generated protocol from Pigeon that represents Flutter messages that can be called from Swift.
+protocol PersonalizationFlutterApiProtocol {
+ func onPushReceived(payload payloadArg: [String: String?], completion: @escaping (Result) -> Void)
+ func onPushDelivered(payload payloadArg: [String: String?], completion: @escaping (Result) -> Void)
+ func onPushClicked(payload payloadArg: [String: String?], completion: @escaping (Result) -> Void)
+}
+class PersonalizationFlutterApi: PersonalizationFlutterApiProtocol {
+ private let binaryMessenger: FlutterBinaryMessenger
+ private let messageChannelSuffix: String
+ init(binaryMessenger: FlutterBinaryMessenger, messageChannelSuffix: String = "") {
+ self.binaryMessenger = binaryMessenger
+ self.messageChannelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : ""
+ }
+ var codec: PersonalizationApiPigeonCodec {
+ return PersonalizationApiPigeonCodec.shared
+ }
+ func onPushReceived(payload payloadArg: [String: String?], completion: @escaping (Result) -> Void) {
+ let channelName: String = "dev.flutter.pigeon.personalization_flutter_sdk.PersonalizationFlutterApi.onPushReceived\(messageChannelSuffix)"
+ let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec)
+ channel.sendMessage([payloadArg] as [Any?]) { response in
+ guard let listResponse = response as? [Any?] else {
+ completion(.failure(createConnectionError(withChannelName: channelName)))
+ return
+ }
+ if listResponse.count > 1 {
+ let code: String = listResponse[0] as! String
+ let message: String? = nilOrValue(listResponse[1])
+ let details: String? = nilOrValue(listResponse[2])
+ completion(.failure(PigeonError(code: code, message: message, details: details)))
+ } else {
+ completion(.success(()))
+ }
+ }
+ }
+ func onPushDelivered(payload payloadArg: [String: String?], completion: @escaping (Result) -> Void) {
+ let channelName: String = "dev.flutter.pigeon.personalization_flutter_sdk.PersonalizationFlutterApi.onPushDelivered\(messageChannelSuffix)"
+ let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec)
+ channel.sendMessage([payloadArg] as [Any?]) { response in
+ guard let listResponse = response as? [Any?] else {
+ completion(.failure(createConnectionError(withChannelName: channelName)))
+ return
+ }
+ if listResponse.count > 1 {
+ let code: String = listResponse[0] as! String
+ let message: String? = nilOrValue(listResponse[1])
+ let details: String? = nilOrValue(listResponse[2])
+ completion(.failure(PigeonError(code: code, message: message, details: details)))
+ } else {
+ completion(.success(()))
+ }
+ }
+ }
+ func onPushClicked(payload payloadArg: [String: String?], completion: @escaping (Result) -> Void) {
+ let channelName: String = "dev.flutter.pigeon.personalization_flutter_sdk.PersonalizationFlutterApi.onPushClicked\(messageChannelSuffix)"
+ let channel = FlutterBasicMessageChannel(name: channelName, binaryMessenger: binaryMessenger, codec: codec)
+ channel.sendMessage([payloadArg] as [Any?]) { response in
+ guard let listResponse = response as? [Any?] else {
+ completion(.failure(createConnectionError(withChannelName: channelName)))
+ return
+ }
+ if listResponse.count > 1 {
+ let code: String = listResponse[0] as! String
+ let message: String? = nilOrValue(listResponse[1])
+ let details: String? = nilOrValue(listResponse[2])
+ completion(.failure(PigeonError(code: code, message: message, details: details)))
+ } else {
+ completion(.success(()))
+ }
+ }
+ }
+}
diff --git a/ios/Resources/PrivacyInfo.xcprivacy b/ios/Resources/PrivacyInfo.xcprivacy
new file mode 100644
index 0000000..a34b7e2
--- /dev/null
+++ b/ios/Resources/PrivacyInfo.xcprivacy
@@ -0,0 +1,14 @@
+
+
+
+
+ NSPrivacyTrackingDomains
+
+ NSPrivacyAccessedAPITypes
+
+ NSPrivacyCollectedDataTypes
+
+ NSPrivacyTracking
+
+
+
diff --git a/ios/personalization_flutter_sdk.podspec b/ios/personalization_flutter_sdk.podspec
new file mode 100644
index 0000000..d384d38
--- /dev/null
+++ b/ios/personalization_flutter_sdk.podspec
@@ -0,0 +1,27 @@
+#
+# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html.
+#
+Pod::Spec.new do |s|
+ s.name = 'personalization_flutter_sdk'
+ s.version = '0.0.1'
+ s.summary = 'Flutter plugin wrapper around REES46 native SDK.'
+ s.description = <<-DESC
+Flutter plugin wrapper around REES46 native SDK.
+ DESC
+ s.homepage = 'http://example.com'
+ s.license = { :file => '../LICENSE' }
+ s.author = { 'REES46' => 'support@rees46.com' }
+ s.source = { :path => '.' }
+ s.source_files = 'Classes/**/*'
+ s.dependency 'Flutter'
+ s.dependency 'REES46', '3.23.0'
+ s.platform = :ios, '13.0'
+
+ # Flutter.framework does not contain a i386 slice.
+ s.pod_target_xcconfig = {
+ 'DEFINES_MODULE' => 'YES',
+ 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386'
+ }
+ s.swift_version = '5.0'
+end
+
diff --git a/lib/personaclick_flutter_sdk.dart b/lib/personaclick_flutter_sdk.dart
new file mode 100644
index 0000000..721c987
--- /dev/null
+++ b/lib/personaclick_flutter_sdk.dart
@@ -0,0 +1,9 @@
+import 'src/personalization_sdk.dart';
+import 'src/sdk_init_config.dart';
+
+export 'src/sdk_init_config.dart' show SdkInitConfig;
+export 'src/tracking/purchase_line_item.dart' show PurchaseLineItem;
+
+typedef PersonaclickInitConfig = SdkInitConfig;
+
+class PersonaclickFlutterSdk extends PersonalizationSdk {}
diff --git a/lib/personalization_flutter_sdk.dart b/lib/personalization_flutter_sdk.dart
new file mode 100644
index 0000000..29cb996
--- /dev/null
+++ b/lib/personalization_flutter_sdk.dart
@@ -0,0 +1,10 @@
+export 'src/personalization_sdk.dart';
+export 'src/profile/profile_params.dart';
+export 'src/recommendation/recommendation_params.dart';
+export 'src/recommendation/recommendation_response.dart';
+export 'src/products/products_list_params.dart';
+export 'src/products/products_list_response.dart';
+export 'src/search/search_params.dart';
+export 'src/search/search_response.dart';
+export 'src/sdk_init_config.dart';
+export 'src/tracking/purchase_line_item.dart';
diff --git a/lib/rees46_flutter_sdk_method_channel.dart b/lib/rees46_flutter_sdk_method_channel.dart
new file mode 100644
index 0000000..43a95a6
--- /dev/null
+++ b/lib/rees46_flutter_sdk_method_channel.dart
@@ -0,0 +1 @@
+export 'src/pigeon/personalization_api.g.dart' show PersonalizationHostApi;
diff --git a/lib/rees46_flutter_sdk_platform_interface.dart b/lib/rees46_flutter_sdk_platform_interface.dart
new file mode 100644
index 0000000..5480116
--- /dev/null
+++ b/lib/rees46_flutter_sdk_platform_interface.dart
@@ -0,0 +1 @@
+export 'src/sdk_init_config.dart' show SdkInitConfig;
diff --git a/lib/src/init/sdk_init_handler.dart b/lib/src/init/sdk_init_handler.dart
new file mode 100644
index 0000000..124c943
--- /dev/null
+++ b/lib/src/init/sdk_init_handler.dart
@@ -0,0 +1,53 @@
+import 'package:flutter/foundation.dart';
+
+import '../pigeon/personalization_api.g.dart' as pigeon;
+import '../sdk_init_config.dart';
+
+class SdkInitHandler {
+ final pigeon.PersonalizationHostApi _api;
+
+ SdkInitHandler({pigeon.PersonalizationHostApi? api})
+ : _api = api ?? pigeon.PersonalizationHostApi();
+
+ Future initialize(SdkInitConfig config) {
+ final apiDomain =
+ (config.apiDomain == null || config.apiDomain!.trim().isEmpty)
+ ? 'api.rees46.ru'
+ : config.apiDomain!.trim();
+
+ final stream = (config.stream == null || config.stream!.trim().isEmpty)
+ ? _defaultStream()
+ : config.stream!.trim();
+
+ final enableLogs = config.enableLogs ?? false;
+ final autoSendPushToken = config.autoSendPushToken ?? true;
+ final sendAdvertisingId = config.sendAdvertisingId ?? false;
+ final enableAutoPopupPresentation =
+ config.enableAutoPopupPresentation ?? true;
+ final needReInitialization = config.needReInitialization ?? false;
+
+ return _api.initialize(
+ pigeon.InitConfig(
+ shopId: config.shopId,
+ apiDomain: apiDomain,
+ stream: stream,
+ enableLogs: enableLogs,
+ autoSendPushToken: autoSendPushToken,
+ sendAdvertisingId: sendAdvertisingId,
+ enableAutoPopupPresentation: enableAutoPopupPresentation,
+ needReInitialization: needReInitialization,
+ ),
+ );
+ }
+
+ static String _defaultStream() {
+ switch (defaultTargetPlatform) {
+ case TargetPlatform.android:
+ return 'android';
+ case TargetPlatform.iOS:
+ return 'ios';
+ default:
+ return 'android';
+ }
+ }
+}
diff --git a/lib/src/method_channel.dart b/lib/src/method_channel.dart
new file mode 100644
index 0000000..7ffd13d
--- /dev/null
+++ b/lib/src/method_channel.dart
@@ -0,0 +1 @@
+// Pigeon is used for platform communication.
diff --git a/lib/src/personalization_sdk.dart b/lib/src/personalization_sdk.dart
new file mode 100644
index 0000000..be5924c
--- /dev/null
+++ b/lib/src/personalization_sdk.dart
@@ -0,0 +1,209 @@
+import 'dart:convert';
+
+import 'pigeon/personalization_api.g.dart' as pigeon;
+import 'init/sdk_init_handler.dart';
+import 'profile/profile_params.dart';
+import 'push/push_notification_callbacks.dart';
+import 'recommendation/recommendation_params.dart';
+import 'recommendation/recommendation_response.dart';
+import 'products/products_list_params.dart';
+import 'products/products_list_response.dart';
+import 'search/search_params.dart';
+import 'search/search_response.dart';
+import 'sdk_init_config.dart';
+import 'tracking/purchase_line_item.dart';
+
+class PersonalizationSdk {
+ final pigeon.PersonalizationHostApi _api;
+ final SdkInitHandler _initHandler;
+ final PushNotificationCallbacks _pushCallbacks = PushNotificationCallbacks();
+
+ PersonalizationSdk({pigeon.PersonalizationHostApi? api})
+ : _api = api ?? pigeon.PersonalizationHostApi(),
+ _initHandler = SdkInitHandler(api: api) {
+ pigeon.PersonalizationFlutterApi.setUp(_pushCallbacks);
+ }
+
+ /// Registers optional listeners for push lifecycle events emitted by native code.
+ void setPushNotificationCallbacks({
+ void Function(Map payload)? onReceived,
+ void Function(Map payload)? onDelivered,
+ void Function(Map payload)? onClicked,
+ }) {
+ _pushCallbacks.setCallbacks(
+ onReceived: onReceived,
+ onDelivered: onDelivered,
+ onClicked: onClicked,
+ );
+ }
+
+ Future getPlatformVersion() {
+ return _api.getPlatformVersion();
+ }
+
+ Future getStoredPushToken() {
+ return _api.getStoredPushToken();
+ }
+
+ Future initialize(SdkInitConfig config) {
+ return _initHandler.initialize(config);
+ }
+
+ Future setProfile(ProfileParams params) {
+ return _api.setProfile(params.toWire());
+ }
+
+ Future getSid() {
+ return _api.getSid();
+ }
+
+ Future getDid() {
+ return _api.getDid();
+ }
+
+ Future getProductInfo(String itemId) async {
+ if (itemId.isEmpty) {
+ throw ArgumentError.value(itemId, 'itemId', 'must be non-empty');
+ }
+ final json = await _api.getProductInfo(itemId);
+ return Product.fromJson(jsonDecode(json) as Map);
+ }
+
+ Future getProductsList({
+ ProductsListParams? params,
+ }) async {
+ final json = await _api.getProductsList(params?.toJson());
+ return ProductsListResponse.fromJson(
+ jsonDecode(json) as Map,
+ );
+ }
+
+ Future searchBlank() async {
+ final json = await _api.searchBlank();
+ return SearchBlankResponse.fromJson(
+ jsonDecode(json) as Map,
+ );
+ }
+
+ Future searchInstant(
+ String query, {
+ SearchInstantParams? params,
+ }) async {
+ if (query.isEmpty) {
+ throw ArgumentError.value(query, 'query', 'must be non-empty');
+ }
+ final json = await _api.searchInstant(query, params?.toJson());
+ return SearchInstantResponse.fromJson(
+ jsonDecode(json) as Map,
+ );
+ }
+
+ Future searchFull(
+ String query, {
+ SearchParams? params,
+ }) async {
+ if (query.isEmpty) {
+ throw ArgumentError.value(query, 'query', 'must be non-empty');
+ }
+ final json = await _api.searchFull(query, params?.toJson());
+ return SearchFullResponse.fromJson(
+ jsonDecode(json) as Map,
+ );
+ }
+
+ Future getRecommendation(
+ String code, {
+ RecommendationParams? params,
+ }) async {
+ if (code.isEmpty) {
+ throw ArgumentError.value(code, 'code', 'must be non-empty');
+ }
+ final json = await _api.getRecommendation(code, params?.toJson());
+ return RecommendationResponse.fromJson(
+ jsonDecode(json) as Map,
+ );
+ }
+
+ /// Custom event tracking (native `trackEvent` / `TrackEventManager.trackEvent`).
+ Future trackEvent(
+ String event, {
+ int? time,
+ String? category,
+ String? label,
+ int? value,
+ Map? customFields,
+ }) {
+ if (event.isEmpty) {
+ throw ArgumentError.value(event, 'event', 'must be non-empty');
+ }
+ final customFieldsJson = customFields == null
+ ? null
+ : jsonEncode(customFields);
+ return _api.trackEvent(
+ event,
+ time,
+ category,
+ label,
+ value,
+ customFieldsJson,
+ );
+ }
+
+ /// Purchase tracking (native `trackPurchase` with typed line items).
+ Future trackPurchase({
+ required String orderId,
+ required double orderPrice,
+ required List items,
+ String? deliveryType,
+ String? deliveryAddress,
+ String? paymentType,
+ bool isTaxFree = false,
+ String? promocode,
+ double? orderCash,
+ double? orderBonuses,
+ double? orderDelivery,
+ double? orderDiscount,
+ String? channel,
+ Map? custom,
+ Map? recommendedSource,
+ String? stream,
+ String? segment,
+ }) {
+ if (orderId.isEmpty) {
+ throw ArgumentError.value(orderId, 'orderId', 'must be non-empty');
+ }
+ if (items.isEmpty) {
+ throw ArgumentError.value(items, 'items', 'must be non-empty');
+ }
+ final wireItems = items
+ .map(
+ (e) => pigeon.PurchaseLineItemWire(
+ id: e.id,
+ amount: e.amount,
+ price: e.price,
+ lineId: e.lineId,
+ fashionSize: e.fashionSize,
+ ),
+ )
+ .toList();
+ return _api.trackPurchase(
+ orderId,
+ orderPrice,
+ wireItems,
+ deliveryType,
+ deliveryAddress,
+ paymentType,
+ isTaxFree,
+ promocode,
+ orderCash,
+ orderBonuses,
+ orderDelivery,
+ orderDiscount,
+ channel,
+ custom == null ? null : jsonEncode(custom),
+ recommendedSource == null ? null : jsonEncode(recommendedSource),
+ stream,
+ segment,
+ );
+ }
+}
diff --git a/lib/src/pigeon/personalization_api.g.dart b/lib/src/pigeon/personalization_api.g.dart
new file mode 100644
index 0000000..2d2afc9
--- /dev/null
+++ b/lib/src/pigeon/personalization_api.g.dart
@@ -0,0 +1,972 @@
+// Autogenerated from Pigeon (v25.5.0), do not edit directly.
+// See also: https://pub.dev/packages/pigeon
+// ignore_for_file: public_member_api_docs, non_constant_identifier_names, avoid_as, unused_import, unnecessary_parenthesis, prefer_null_aware_operators, omit_local_variable_types, unused_shown_name, unnecessary_import, no_leading_underscores_for_local_identifiers
+
+import 'dart:async';
+import 'dart:typed_data' show Float64List, Int32List, Int64List, Uint8List;
+
+import 'package:flutter/foundation.dart' show ReadBuffer, WriteBuffer;
+import 'package:flutter/services.dart';
+
+PlatformException _createConnectionError(String channelName) {
+ return PlatformException(
+ code: 'channel-error',
+ message: 'Unable to establish connection on channel: "$channelName".',
+ );
+}
+
+List