Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/mbx 2792 inapps negative validation #264

Merged
merged 10 commits into from
Sep 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
208 changes: 176 additions & 32 deletions Mindbox.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions Mindbox/DI/DependencyContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ protocol DependencyContainer {
var imageDownloadService: ImageDownloadServiceProtocol { get }
var abTestDeviceMixer: ABTestDeviceMixer { get }
var urlExtractorService: VariantImageUrlExtractorService { get }
var inappFilterService: InappFilterProtocol { get }
}

protocol InstanceFactory {
Expand Down
16 changes: 15 additions & 1 deletion Mindbox/DI/DependencyProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ final class DependencyProvider: DependencyContainer {
var imageDownloadService: ImageDownloadServiceProtocol
var abTestDeviceMixer: ABTestDeviceMixer
var urlExtractorService: VariantImageUrlExtractorService
var inappFilterService: InappFilterProtocol

init() throws {
utilitiesFetcher = MBUtilitiesFetcher()
Expand Down Expand Up @@ -69,11 +70,24 @@ final class DependencyProvider: DependencyContainer {
let presentationManager = InAppPresentationManager(actionHandler: actionHandler,
displayUseCase: displayUseCase)
urlExtractorService = VariantImageUrlExtractorService()
let actionFilter = LayerActionFilterService()
let sourceFilter = LayersSourceFilterService()
let layersFilterService = LayersFilterService(actionFilter: actionFilter, sourceFilter: sourceFilter)
let sizeFilter = ElementSizeFilterService()
let colorFilter = ElementsColorFilterService()
let positionFilter = ElementsPositionFilterService()
let elementsFilterService = ElementsFilterService(sizeFilter: sizeFilter, positionFilter: positionFilter, colorFilter: colorFilter)
let contentPositionFilterService = ContentPositionFilterService()
let variantsFilterService = VariantFilterService(layersFilter: layersFilterService,
elementsFilter: elementsFilterService,
contentPositionFilter: contentPositionFilterService)
inappFilterService = InappsFilterService(variantsFilter: variantsFilterService)
inAppMessagesManager = InAppCoreManager(
configManager: InAppConfigurationManager(
inAppConfigAPI: InAppConfigurationAPI(persistenceStorage: persistenceStorage),
inAppConfigRepository: InAppConfigurationRepository(),
inAppConfigurationMapper: InAppConfigutationMapper(geoService: geoService,
inAppConfigurationMapper: InAppConfigutationMapper(inappFilterService: inappFilterService,
geoService: geoService,
segmentationService: segmentationSevice,
customerSegmentsAPI: .live,
targetingChecker: inAppTargetingChecker,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ protocol InAppConfigurationMapperProtocol {

final class InAppConfigutationMapper: InAppConfigurationMapperProtocol {

private let inappFilterService: InappFilterProtocol
private let geoService: GeoServiceProtocol
private let segmentationService: SegmentationServiceProtocol
private let customerSegmentsAPI: CustomerSegmentsAPI
Expand All @@ -30,7 +31,8 @@ final class InAppConfigutationMapper: InAppConfigurationMapperProtocol {

private let dispatchGroup = DispatchGroup()

init(geoService: GeoServiceProtocol,
init(inappFilterService: InappFilterProtocol,
geoService: GeoServiceProtocol,
segmentationService: SegmentationServiceProtocol,
customerSegmentsAPI: CustomerSegmentsAPI,
targetingChecker: InAppTargetingCheckerProtocol,
Expand All @@ -40,6 +42,7 @@ final class InAppConfigutationMapper: InAppConfigurationMapperProtocol {
imageDownloadService: ImageDownloadServiceProtocol,
urlExtractorService: VariantImageUrlExtractorServiceProtocol,
abTestDeviceMixer: ABTestDeviceMixer) {
self.inappFilterService = inappFilterService
self.geoService = geoService
self.segmentationService = segmentationService
self.customerSegmentsAPI = customerSegmentsAPI
Expand All @@ -57,7 +60,8 @@ final class InAppConfigutationMapper: InAppConfigurationMapperProtocol {
_ response: ConfigResponse,
_ completion: @escaping (InAppFormData?) -> Void) {
let shownInAppsIds = Set(persistenceStorage.shownInAppsIds ?? [])
let responseInapps = filterInappsByABTests(response.abtests, responseInapps: response.inapps?.elements)
let inapps = inappFilterService.filter(inapps: response.inapps?.elements)
let responseInapps = filterInappsByABTests(response.abtests, responseInapps: inapps)
let filteredInapps = filterInappsBySDKVersion(responseInapps, shownInAppsIds: shownInAppsIds)
Logger.common(message: "Shown in-apps ids: [\(shownInAppsIds)]", level: .info, category: .inAppMessages)
if filteredInapps.isEmpty {
Expand Down Expand Up @@ -242,7 +246,7 @@ final class InAppConfigutationMapper: InAppConfigurationMapperProtocol {
}

var inAppsForEvent = filteredInAppsByEvent[triggerEvent] ?? [InAppTransitionData]()
if let inAppFormVariants = inapp.form.variants.elements.first {
if let inAppFormVariants = inapp.form.variants.first {
let formData = InAppTransitionData(inAppId: inapp.id,
content: inAppFormVariants)
inAppsForEvent.append(formData)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
//
// ContentPositionFilter.swift
// Mindbox
//
// Created by vailence on 07.09.2023.
// Copyright © 2023 Mindbox. All rights reserved.
//

import Foundation
import MindboxLogger

protocol ContentPositionFilterProtocol {
func filter(_ contentPosition: ContentPositionDTO?) throws -> ContentPosition
}

final class ContentPositionFilterService: ContentPositionFilterProtocol {

enum Constants {
static let defaultGravity = ContentPositionGravity(vertical: .bottom, horizontal: .center)
static let defaultMargin = ContentPositionMargin(kind: .dp, top: 0, right: 0, left: 0, bottom: 0)
static let defaultContentPosition = ContentPosition(gravity: defaultGravity, margin: defaultMargin)
}

func filter(_ contentPosition: ContentPositionDTO?) throws -> ContentPosition {
guard let contentPosition = contentPosition else {
Logger.common(message: "Content position is invalid or missing. Default value set: [\(Constants.defaultContentPosition)].", level: .debug, category: .inAppMessages)
return Constants.defaultContentPosition
}

var customGravity: ContentPositionGravity
if let gravity = contentPosition.gravity {
let vertical = gravity.vertical ?? .bottom
let horizontal = gravity.horizontal ?? .center
customGravity = ContentPositionGravity(vertical: vertical, horizontal: horizontal)
} else {
Logger.common(message: "Gravity is invalid or missing. Default value set: [\(Constants.defaultGravity)].", level: .debug, category: .inAppMessages)
customGravity = Constants.defaultGravity
}

var customMargin: ContentPositionMargin?
if let margin = contentPosition.margin {
switch margin.kind {
case .dp:
if let top = margin.top,
let left = margin.left,
let right = margin.right,
let bottom = margin.bottom,
top >= 0,
left >= 0,
right >= 0,
bottom >= 0 {
customMargin = ContentPositionMargin(kind: margin.kind, top: top, right: right, left: left, bottom: bottom)
}
case .unknown:
Logger.common(message: "Content position margin kind is unknown. Default value set: [\(Constants.defaultMargin)].", level: .debug, category: .inAppMessages)
customMargin = Constants.defaultMargin
}
} else {
Logger.common(message: "Content position margin is invalid or missing. Default value set: [\(Constants.defaultMargin)].", level: .debug, category: .inAppMessages)
customMargin = Constants.defaultMargin
}

guard let customMargin = customMargin else {
throw CustomDecodingError.unknownType("ContentPositionFilterService validation not passed. Inapp will be skipped.")
}

return ContentPosition(gravity: customGravity, margin: customMargin)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//
// ElementsColorFilterService.swift
// Mindbox
//
// Created by vailence on 15.09.2023.
// Copyright © 2023 Mindbox. All rights reserved.
//

import Foundation
import MindboxLogger

protocol ElementsColorFilterProtocol {
func filter(_ color: String?) throws -> String
}

final class ElementsColorFilterService: ElementsColorFilterProtocol {
enum Constants {
static let defaultColor = "#FFFFFF"
}

func filter(_ color: String?) throws -> String {
guard let color = color, color.isHexValid() else {
Logger.common(message: "Color is invalid or missing. Default value set: [\(Constants.defaultColor)]", level: .debug, category: .inAppMessages)
return Constants.defaultColor
}

return color
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
//
// ElementsFilter.swift
// Mindbox
//
// Created by vailence on 07.09.2023.
// Copyright © 2023 Mindbox. All rights reserved.
//

import Foundation
import MindboxLogger

protocol ElementsFilterProtocol {
func filter(_ elements: [ContentElementDTO]?) throws -> [ContentElement]
}

final class ElementsFilterService: ElementsFilterProtocol {

enum Constants {
static let lineWidth = 2
}

private let sizeFilter: ElementsSizeFilterProtocol
private let positionFilter: ElementsPositionFilterProtocol
private let colorFilter: ElementsColorFilterProtocol

init(sizeFilter: ElementsSizeFilterProtocol, positionFilter: ElementsPositionFilterProtocol, colorFilter: ElementsColorFilterProtocol) {
self.sizeFilter = sizeFilter
self.positionFilter = positionFilter
self.colorFilter = colorFilter
}

func filter(_ elements: [ContentElementDTO]?) throws -> [ContentElement] {
guard let elements = elements, !elements.isEmpty else {
Logger.common(message: "Elements are missing or empty.", level: .debug, category: .inAppMessages)
return []
}

var filteredElements: [ContentElement] = []

elementsLoop: for element in elements {
if element.elementType == .unknown {
continue
}

switch element {
case .closeButton(let closeButtonElementDTO):
let size = try sizeFilter.filter(closeButtonElementDTO.size)
let position = try positionFilter.filter(closeButtonElementDTO.position)
let color = try colorFilter.filter(closeButtonElementDTO.color?.element)
var lineWidth: Int
if let lineWidthDTO = closeButtonElementDTO.lineWidth?.element {
lineWidth = lineWidthDTO
} else {
lineWidth = Constants.lineWidth
Logger.common(message: "Line width is invalid or missing. Default value set: [\(Constants.lineWidth)].", level: .debug, category: .inAppMessages)
}
let customCloseButtonElement = CloseButtonElement(color: color,
lineWidth: lineWidth,
size: size,
position: position)
let element = try ContentElement(type: .closeButton, closeButton: customCloseButtonElement)
filteredElements.append(element)
case .unknown:
continue elementsLoop
}
}

return filteredElements
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
//
// ElementsPositionFilter.swift
// Mindbox
//
// Created by vailence on 07.09.2023.
// Copyright © 2023 Mindbox. All rights reserved.
//

import Foundation
import MindboxLogger

protocol ElementsPositionFilterProtocol {
func filter(_ position: ContentElementPositionDTO?) throws -> ContentElementPosition
}

final class ElementsPositionFilterService: ElementsPositionFilterProtocol {

enum Constants {
static let defaultMargin = ContentElementPositionMargin(kind: .proportion, top: 0.02, right: 0.02, left: 0.02, bottom: 0.02)

}

func filter(_ position: ContentElementPositionDTO?) throws -> ContentElementPosition {
guard let position = position,
let margin = position.margin else {
Logger.common(message: "Position or margin is invalid or missing. Default value set: [\(Constants.defaultMargin)].", level: .debug, category: .inAppMessages)
return ContentElementPosition(margin: Constants.defaultMargin)
}

let marginRange: ClosedRange<Double> = 0...1

switch margin.kind {
case .proportion:
if let top = margin.top,
let left = margin.left,
let right = margin.right,
let bottom = margin.bottom,
marginRange.contains(top),
marginRange.contains(left),
marginRange.contains(right),
marginRange.contains(bottom) {
let customMargin = ContentElementPositionMargin(kind: margin.kind,
top: top,
right: right,
left: left,
bottom: bottom)
return ContentElementPosition(margin: customMargin)
}
case .unknown:
Logger.common(message: "Unknown type of ContentElementPosition. Default value set: [\(Constants.defaultMargin)].", level: .debug, category: .inAppMessages)
return ContentElementPosition(margin: Constants.defaultMargin)
}

throw CustomDecodingError.unknownType("ElementsPositionFilterService validation not passed. In-app will be ignored.")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
//
// ElementsSizeFilter.swift
// Mindbox
//
// Created by vailence on 07.09.2023.
// Copyright © 2023 Mindbox. All rights reserved.
//

import Foundation
import MindboxLogger

protocol ElementsSizeFilterProtocol {
func filter(_ size: ContentElementSizeDTO?) throws -> ContentElementSize
}

final class ElementSizeFilterService: ElementsSizeFilterProtocol {
enum Constants {
static let defaultSize = ContentElementSize(kind: .dp, width: 24, height: 24)
}

func filter(_ size: ContentElementSizeDTO?) throws -> ContentElementSize {
guard let size = size else {
Logger.common(message: "Size is invalid or missing. Default value set: [\(Constants.defaultSize)].", level: .debug, category: .inAppMessages)
return Constants.defaultSize
}

switch size.kind {
case .dp:
if let height = size.height,
let width = size.width,
height >= 0,
width >= 0 {
return ContentElementSize(kind: size.kind, width: width, height: height)
}
case .unknown:
Logger.common(message: "Unknown type of ContentElementSize. Default value set: [\(Constants.defaultSize)].", level: .debug, category: .inAppMessages)
return Constants.defaultSize
}

throw CustomDecodingError.unknownType("ElementSizeFilterService validation not passed. In-app will be ignored.")
}
}
Loading