Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
75 commits
Select commit Hold shift + click to select a range
2643284
MOBILEWEBVIEW-3: Add mindbox webview
enotniy Jan 20, 2026
0e0c490
MOBILEWEBVIEW-3: Fix lint error
enotniy Jan 20, 2026
b7a43d9
MOBILEWEBVIEW-3: Fix lint error
enotniy Jan 20, 2026
8e1795f
MOBILEWEBVIEW-3: Follow code review
enotniy Jan 21, 2026
6bb0c86
MOBILEWEBVIEW-3: Refactoring
enotniy Jan 21, 2026
961f777
Merge pull request #669 from mindbox-cloud/feature/MOBILEWEBVIEW-3
enotniy Jan 21, 2026
3a50ab0
MOBILEWEBVIEW-6: Add js brige
enotniy Feb 2, 2026
014d236
MOBILEWEBVIEW-6: Add message validator
enotniy Feb 2, 2026
2ca8c4b
MOBILEWEBVIEW-6: Add tests
enotniy Feb 2, 2026
7406683
MOBILEWEBVIEW-6: Update common sdk
enotniy Feb 2, 2026
0473969
MOBILEWEBVIEW-6: Follow code review
enotniy Feb 2, 2026
933cf11
Merge pull request #671 from mindbox-cloud/feature/MOBILEWEBVIEW-6
enotniy Feb 3, 2026
e1412f8
MOBILEWEBVIEW-31: support featureToggle section in config
sergeysozinov Feb 4, 2026
8d9a627
MOBILEWEBVIEW-31: change default logic (#673)
sergeysozinov Feb 6, 2026
d136817
MOBILEWEBVIEW-7: add back action
enotniy Feb 6, 2026
36d8bf0
MOBILEWEBVIEW-7: refactoring contentUrl request
enotniy Feb 6, 2026
1bb636b
Merge pull request #674 from mindbox-cloud/feature/MOBILEWEBVIEW-7
enotniy Feb 6, 2026
1065d40
WMSDK-608: Support app distribution from all branches (#670)
justSmK Feb 9, 2026
e5b54b0
MOBILEWEBVIEW-10: Add Inapp.ShowFailure operation
sergeysozinov Feb 12, 2026
1c4fab0
MOBILEWEBVIEW-3: Fix InAppPositionController for BottomSheet
enotniy Jan 21, 2026
8ae583f
MOBILEWEBVIEW-8: Add data collector
enotniy Feb 11, 2026
6cc3e3c
MOBILEWEBVIEW-8: ADd constant for status
enotniy Feb 12, 2026
a6d3560
MOBILEWEBVIEW-8: Add tests
enotniy Feb 12, 2026
f5df263
MOBILEWEBVIEW-8: Fix ime padding
enotniy Feb 12, 2026
aa56e6b
Merge pull request #677 from mindbox-cloud/feature/MOBILEWEBVIEW-8
enotniy Feb 13, 2026
adad8ae
MOBILEWEBVIEW-10: change field name
sergeysozinov Feb 13, 2026
b820c2a
MOBILEWEBVIEW-8: Fix permissions in ready action
enotniy Feb 16, 2026
fa4a897
MOBILEWEBVIEW-7: Fix back action on reattach webview
enotniy Feb 16, 2026
c2a15f0
Merge pull request #679 from mindbox-cloud/feature/MOBILEWEBVIEW-8
enotniy Feb 16, 2026
a591388
MOBILEWEBVIEW-46: Add sync/async operations
enotniy Feb 16, 2026
4eb8a61
MOBILEWEBVIEW-5: support web layer
sergeysozinov Feb 17, 2026
1343f3e
MOBILEWEBVIEW-46: Follow code review
enotniy Feb 17, 2026
609accd
Merge pull request #681 from mindbox-cloud/feature/MOBILEWEBVIEW-46
enotniy Feb 17, 2026
bb6d08e
MOBILEWEBVIEW-10: fix bugs and delete sending error when no internet
sergeysozinov Feb 19, 2026
569fd39
MOBILEWEBVIEW-54: Fix js bridge
enotniy Feb 18, 2026
44da113
MOBILEWEBVIEW-54: Follow code review
enotniy Feb 19, 2026
1ba8426
Merge pull request #682 from mindbox-cloud/feature/MOBILEWEBVIEW-54
enotniy Feb 19, 2026
32e25a9
MOBILEWEBVIEW-57: Add link router for webview
enotniy Mar 2, 2026
5b9326a
MOBILEWEBVIEW-57: Change error format
enotniy Mar 2, 2026
2410acd
MOBILEWEBVIEW-60: add timeToDisplay for Inapp.Show action
sergeysozinov Mar 2, 2026
29f9031
MOBILEWEBVIEW-57: Fix log
enotniy Mar 2, 2026
e3c3bd4
Merge pull request #685 from mindbox-cloud/feature/MOBILEWEBVIEW-57
enotniy Mar 2, 2026
728ddff
Bump SDK version to 2.15.0-rc
github-actions[bot] Mar 5, 2026
fc8de01
MOBILEWEBVIEW-75: fix back button for modal window
sergeysozinov Mar 5, 2026
1416c0c
MOBILEWEBVIEW-34: Refactoring close inapp
enotniy Mar 4, 2026
1c4c988
MOBILEWEBVIEW-34: Refactoring stop inapp
enotniy Mar 5, 2026
5853c2d
MOBILEWEBVIEW-34: Fix trigger dismiss
enotniy Mar 6, 2026
d7c5085
Merge pull request #687 from mindbox-cloud/feature/MOBILEWEBVIEW-34
enotniy Mar 10, 2026
f2510f1
Merge pull request #688 from mindbox-cloud/release/2.15.0-rc
AndreyEmtsov Mar 10, 2026
55f1f05
MOBILEWEBVIEW-94: Add local state storage
enotniy Mar 11, 2026
c432976
MOBILEWEBVIEW-94: Add test. Add local state version to ready response
enotniy Mar 12, 2026
331229e
MOBILEWEBVIEW-94: Follow code review
enotniy Mar 12, 2026
1ca1652
Merge pull request #690 from mindbox-cloud/feature/MOBILEWEBVIEW-94
enotniy Mar 12, 2026
b1f8eca
MOBILEWEBVIEW-97: add firstInitializationDateTime
sergeysozinov Mar 12, 2026
2ad0d87
MOBILEWEBVIEW-34: Fix remove paused viewholder
enotniy Mar 13, 2026
37927c3
MOBILEWEBVIEW-34: Fix remove inapp for part activity
enotniy Mar 16, 2026
0921383
MOBILEWEBVIEW-34: Fix paused viewholder inapp
enotniy Mar 17, 2026
5db51ac
MOBILEWEBVIEW-98: support vibration
sergeysozinov Mar 18, 2026
bf38a8e
MOBILEWEBVIEW-100: Add permission request for jsbridge
enotniy Mar 18, 2026
2f7e27e
MOBILEWEBVIEW-98: fix unit test stubbing
Mar 18, 2026
fa41ceb
MOBILEWEBVIEW-100: Follow code review
enotniy Mar 18, 2026
717826d
MOBILEWEBVIEW-100: Follow code review
enotniy Mar 18, 2026
84ef639
Merge pull request #694 from mindbox-cloud/feature/MOBILEWEBVIEW-100
enotniy Mar 19, 2026
8d58f66
MOBILEWEBVIEW-133: Add push permission request for jsbridge
enotniy Mar 20, 2026
357905a
MOBILEWEBVIEW-133: Remove activity for repmissions
enotniy Mar 20, 2026
4ffe116
MOBILEWEBVIEW-133: Remove route to settings
enotniy Mar 20, 2026
c4a6fe4
Merge pull request #695 from mindbox-cloud/feature/MOBILEWEBVIEW-133
enotniy Mar 20, 2026
bbc58c5
MOBILE-53: Add settings.open action for js bridge
enotniy Mar 24, 2026
a0e210d
Merge pull request #696 from mindbox-cloud/feature/MOBILE-53
enotniy Mar 24, 2026
700808d
MOBILEWEBVIEW-126: change back action handler
sergeysozinov Mar 25, 2026
d2e4bcd
MOBILEWEBVIEW-133: Fix dialogShown
enotniy Mar 25, 2026
37ba88e
Merge pull request #698 from mindbox-cloud/feature/MOBILEWEBVIEW-133_2
enotniy Mar 26, 2026
2ca8821
MOBILEWEBVIEW-133: Fix open settings after request permission
enotniy Mar 27, 2026
7161112
MOBILE-39: support shake and flip
sergeysozinov Mar 27, 2026
0fee705
Merge branch 'develop' into mission/webview-inapp
enotniy Mar 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
279 changes: 279 additions & 0 deletions WebViewLocalStateStorageTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,279 @@
//
// WebViewLocalStateStorageTests.swift
// MindboxTests
//
// Created by Sergei Semko on 3/11/26.
// Copyright © 2026 Mindbox. All rights reserved.
//

import Testing
@testable import Mindbox

@Suite("WebViewLocalStateStorage", .tags(.webView))
struct WebViewLocalStateStorageTests {

private let testSuiteName = "cloud.Mindbox.test.webview.localState"
private let keyPrefix = Constants.WebViewLocalState.keyPrefix

private func makeSUT() -> (sut: WebViewLocalStateStorage, defaults: UserDefaults, persistence: MockPersistenceStorage) {
let persistence = MockPersistenceStorage()
let defaults = UserDefaults(suiteName: testSuiteName)!
defaults.removePersistentDomain(forName: testSuiteName)
let sut = WebViewLocalStateStorage(dataDefaults: defaults, persistenceStorage: persistence)
return (sut, defaults, persistence)
}

// MARK: - get

@Test("get returns default version and empty data when storage is empty")
func getEmptyStorage() {
let (sut, _, _) = makeSUT()

let state = sut.get(keys: [])

#expect(state.version == Constants.WebViewLocalState.defaultVersion)
#expect(state.data.isEmpty)
}

@Test("get returns all stored keys when keys array is empty")
func getAllKeys() {
let (sut, defaults, _) = makeSUT()
defaults.set("value1", forKey: "\(keyPrefix)key1")
defaults.set("value2", forKey: "\(keyPrefix)key2")

let state = sut.get(keys: [])

#expect(state.data.count == 2)
#expect(state.data["key1"] == "value1")
#expect(state.data["key2"] == "value2")
}

@Test("get returns only requested keys")
func getSpecificKeys() {
let (sut, defaults, _) = makeSUT()
defaults.set("value1", forKey: "\(keyPrefix)key1")
defaults.set("value2", forKey: "\(keyPrefix)key2")
defaults.set("value3", forKey: "\(keyPrefix)key3")

let state = sut.get(keys: ["key1", "key3"])

#expect(state.data.count == 2)
#expect(state.data["key1"] == "value1")
#expect(state.data["key3"] == "value3")
}

@Test("get omits missing keys from data")
func getMissingKeys() {
let (sut, defaults, _) = makeSUT()
defaults.set("value1", forKey: "\(keyPrefix)key1")

let state = sut.get(keys: ["key1", "missing"])

#expect(state.data.count == 1)
#expect(state.data["key1"] == "value1")
#expect(state.data["missing"] == nil)
}

@Test("get returns current version from persistence")
func getCurrentVersion() {
let (sut, _, persistence) = makeSUT()
persistence.webViewLocalStateVersion = 5

let state = sut.get(keys: [])

#expect(state.version == 5)
}

@Test("get returns default version when persistence version is nil")
func getDefaultVersion() {
let (sut, _, persistence) = makeSUT()
persistence.webViewLocalStateVersion = nil

let state = sut.get(keys: [])

#expect(state.version == Constants.WebViewLocalState.defaultVersion)
}

// MARK: - set

@Test("set stores values in UserDefaults")
func setStoresValues() {
let (sut, defaults, _) = makeSUT()

_ = sut.set(data: ["key1": "value1", "key2": "value2"])

#expect(defaults.string(forKey: "\(keyPrefix)key1") == "value1")
#expect(defaults.string(forKey: "\(keyPrefix)key2") == "value2")
}

@Test("set removes key when value is nil")
func setRemovesNilKey() {
let (sut, defaults, _) = makeSUT()
defaults.set("value1", forKey: "\(keyPrefix)key1")

_ = sut.set(data: ["key1": nil])

#expect(defaults.string(forKey: "\(keyPrefix)key1") == nil)
}

@Test("set updates existing values")
func setUpdatesValues() {
let (sut, defaults, _) = makeSUT()
defaults.set("old", forKey: "\(keyPrefix)key1")

let state = sut.set(data: ["key1": "new"])

#expect(defaults.string(forKey: "\(keyPrefix)key1") == "new")
#expect(state.data["key1"] == "new")
}

@Test("set returns only affected keys")
func setReturnsAffectedKeys() {
let (sut, defaults, _) = makeSUT()
defaults.set("existing", forKey: "\(keyPrefix)existing")

let state = sut.set(data: ["key1": "value1"])

#expect(state.data.count == 1)
#expect(state.data["key1"] == "value1")
#expect(state.data["existing"] == nil)
}

@Test("set does not change version")
func setPreservesVersion() {
let (sut, _, persistence) = makeSUT()
persistence.webViewLocalStateVersion = 3

let state = sut.set(data: ["key1": "value1"])

#expect(state.version == 3)
#expect(persistence.webViewLocalStateVersion == 3)
}

@Test("set stores each key as separate UserDefaults entry")
func setSeparateEntries() {
let (sut, defaults, _) = makeSUT()

_ = sut.set(data: ["firstKey": "firstValue", "secondKey": "secondValue"])

#expect(defaults.string(forKey: "\(keyPrefix)firstKey") == "firstValue")
#expect(defaults.string(forKey: "\(keyPrefix)secondKey") == "secondValue")
}

// MARK: - initialize

@Test("initialize stores version in PersistenceStorage")
func initStoresVersion() {
let (sut, _, persistence) = makeSUT()

_ = sut.initialize(version: 7, data: ["key": "value"])

#expect(persistence.webViewLocalStateVersion == 7)
}

@Test("initialize stores data and returns it")
func initStoresAndReturnsData() throws {
let (sut, defaults, _) = makeSUT()

let state = try #require(sut.initialize(version: 2, data: ["key1": "value1", "key2": "value2"]))

#expect(state.version == 2)
#expect(state.data["key1"] == "value1")
#expect(state.data["key2"] == "value2")
#expect(defaults.string(forKey: "\(keyPrefix)key1") == "value1")
#expect(defaults.string(forKey: "\(keyPrefix)key2") == "value2")
}

@Test("initialize rejects zero version")
func initRejectsZero() {
let (sut, _, _) = makeSUT()

#expect(sut.initialize(version: 0, data: ["key": "value"]) == nil)
}

@Test("initialize rejects negative version")
func initRejectsNegative() {
let (sut, _, _) = makeSUT()

#expect(sut.initialize(version: -1, data: ["key": "value"]) == nil)
}

@Test("initialize removes keys with nil values")
func initRemovesNilKeys() {
let (sut, defaults, _) = makeSUT()
defaults.set("value1", forKey: "\(keyPrefix)key1")

let state = sut.initialize(version: 2, data: ["key1": nil])

#expect(state != nil)
#expect(defaults.string(forKey: "\(keyPrefix)key1") == nil)
}

@Test("initialize merges with existing data")
func initMergesData() {
let (sut, defaults, _) = makeSUT()
defaults.set("existing", forKey: "\(keyPrefix)old")

let state = sut.initialize(version: 3, data: ["new": "value"])

#expect(state != nil)
#expect(defaults.string(forKey: "\(keyPrefix)old") == "existing")
#expect(defaults.string(forKey: "\(keyPrefix)new") == "value")
}

@Test("initialize does not store version on rejection")
func initPreservesVersionOnReject() {
let (sut, _, persistence) = makeSUT()
persistence.webViewLocalStateVersion = 5

_ = sut.initialize(version: 0, data: ["key": "value"])

#expect(persistence.webViewLocalStateVersion == 5)
}

// MARK: - Integration

@Test("full flow: init → set → get")
func fullFlow() throws {
let (sut, _, _) = makeSUT()

let initState = try #require(sut.initialize(version: 2, data: ["key1": "value1", "key2": "value2"]))
#expect(initState.version == 2)

let setState = sut.set(data: ["key1": "updated", "key2": nil, "key3": "value3"])
#expect(setState.version == 2)

let getState = sut.get(keys: [])
#expect(getState.version == 2)
#expect(getState.data["key1"] == "updated")
#expect(getState.data["key2"] == nil)
#expect(getState.data["key3"] == "value3")
}

@Test("get after set with null returns empty for deleted key")
func setNullThenGet() {
let (sut, _, _) = makeSUT()

_ = sut.set(data: ["key1": "value1"])
_ = sut.set(data: ["key1": nil])

let state = sut.get(keys: ["key1"])
#expect(state.data.isEmpty)
}

@Test("prefix isolation: non-prefixed keys and Apple system keys are filtered out")
func prefixIsolation() {
let (sut, defaults, _) = makeSUT()
defaults.set("foreign", forKey: "foreignKey")
defaults.set("value", forKey: "\(keyPrefix)myKey")

let state = sut.get(keys: [])

#expect(state.data.count == 1)
#expect(state.data["myKey"] == "value")
#expect(state.data["foreignKey"] == nil)
#expect(state.data["AKLastLocale"] == nil)
#expect(state.data["AppleLocale"] == nil)
#expect(state.data["NSInterfaceStyle"] == nil)
}
}
2 changes: 1 addition & 1 deletion example/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ dependencies {
implementation 'com.google.code.gson:gson:2.11.0'

//Mindbox
implementation 'cloud.mindbox:mobile-sdk:2.14.5'
implementation 'cloud.mindbox:mobile-sdk:2.15.0-rc'
implementation 'cloud.mindbox:mindbox-firebase'
implementation 'cloud.mindbox:mindbox-huawei'
implementation 'cloud.mindbox:mindbox-rustore'
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ android.enableJetifier=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
# SDK version property
SDK_VERSION_NAME=2.14.5
SDK_VERSION_NAME=2.15.0-rc
USE_LOCAL_MINDBOX_COMMON=true
android.nonTransitiveRClass=false
kotlin.mpp.androidGradlePluginCompatibility.nowarn=true
Expand Down
1 change: 1 addition & 0 deletions sdk/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="com.google.android.gms.permission.AD_ID"/>
<uses-permission android:name="android.permission.VIBRATE" />

<uses-sdk tools:overrideLibrary="io.mockk, io.mockk.proxy.android" />
<application>
Expand Down
17 changes: 17 additions & 0 deletions sdk/src/main/java/cloud/mindbox/mobile_sdk/Extensions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import cloud.mindbox.mobile_sdk.Mindbox.logE
import cloud.mindbox.mobile_sdk.Mindbox.logW
import cloud.mindbox.mobile_sdk.inapp.domain.models.InApp
import cloud.mindbox.mobile_sdk.inapp.domain.models.InAppType
import cloud.mindbox.mobile_sdk.inapp.domain.models.Layer
import cloud.mindbox.mobile_sdk.logger.MindboxLoggerImpl
import cloud.mindbox.mobile_sdk.pushes.PushNotificationManager.EXTRA_UNIQ_PUSH_BUTTON_KEY
import cloud.mindbox.mobile_sdk.pushes.PushNotificationManager.EXTRA_UNIQ_PUSH_KEY
Expand Down Expand Up @@ -299,3 +300,19 @@ internal fun List<InApp>.sortByPriority(): List<InApp> {
internal inline fun <T> Queue<T>.pollIf(predicate: (T) -> Boolean): T? {
return peek()?.takeIf(predicate)?.let { poll() }
}

internal fun InAppType.getImageUrl(): String? {
return when (this) {
is InAppType.WebView -> this.layers
is InAppType.ModalWindow -> this.layers
is InAppType.Snackbar -> this.layers
}
.filterIsInstance<Layer.ImageLayer>()
.firstOrNull()
?.source
?.let { source ->
when (source) {
is Layer.ImageLayer.Source.UrlSource -> source.url
}
}
}
10 changes: 9 additions & 1 deletion sdk/src/main/java/cloud/mindbox/mobile_sdk/Mindbox.kt
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ public object Mindbox : MindboxLog {
private lateinit var lifecycleManager: LifecycleManager

private val userVisitManager: UserVisitManager by mindboxInject { userVisitManager }
private val timeProvider by mindboxInject { timeProvider }

internal var pushServiceHandlers: List<PushServiceHandler> = listOf()

Expand Down Expand Up @@ -1244,6 +1245,11 @@ public object Mindbox : MindboxLog {
MindboxPreferences.isNotificationEnabled = isNotificationEnabled
MindboxPreferences.instanceId = instanceId

if (MindboxPreferences.firstInitializationTime == null) {
MindboxPreferences.firstInitializationTime = timeProvider.currentTimestamp()
.convertToIso8601String()
}

MindboxEventManager.appInstalled(context, initData, configuration.shouldCreateCustomer)

deliverDeviceUuid(deviceUuid)
Expand Down Expand Up @@ -1358,7 +1364,9 @@ public object Mindbox : MindboxLog {
requestUrl = requestUrl,
sdkVersionNumeric = Constants.SDK_VERSION_NUMERIC
)

if (source != null || requestUrl != null) {
sessionStorageManager.lastTrackVisitData = trackVisitData
}
MindboxEventManager.appStarted(applicationContext, trackVisitData)
}
}
Expand Down
Loading
Loading