From bfe23996fb78a8622f7def2a6ee6d2839a4acc92 Mon Sep 17 00:00:00 2001 From: Akylbek Utekeshev Date: Fri, 29 Mar 2024 00:39:51 +0500 Subject: [PATCH 01/35] MBX-0000 Auto create and bump release branch --- .github/workflows/prepare_release_branch.yml | 83 ++++++++++++++++++++ git-release-branch-create.sh | 7 +- 2 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/prepare_release_branch.yml diff --git a/.github/workflows/prepare_release_branch.yml b/.github/workflows/prepare_release_branch.yml new file mode 100644 index 00000000..9447361b --- /dev/null +++ b/.github/workflows/prepare_release_branch.yml @@ -0,0 +1,83 @@ +name: Preparation for release + +on: + push: + branches: + - 'release/*' + +jobs: + preparation: + runs-on: macos-14 + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Extract version from branch name + run: echo "VERSION=$(echo ${GITHUB_REF#refs/heads/release/})" >> $GITHUB_ENV + + - name: Check if sdkVersion matches VERSION + run: | + SDK_VERSION=$(sed -n 's/^.*sdkVersion = "\(.*\)"/\1/p' SDKVersionProvider/SDKVersionProvider.swift) + if [ "$SDK_VERSION" != "${{ env.VERSION }}" ]; then + echo "SDK version ($SDK_VERSION) does not match the branch version (${{ env.VERSION }})." + exit 1 + fi + shell: bash + + - name: Bump version + run: ./git-release-branch-create.sh "${{ env.VERSION }}" + + - name: Create Pull Request + id: create-pr + uses: peter-evans/create-pull-request@v4 + with: + commit-message: Update release version to ${{ env.VERSION }} + title: "Release/${{ env.VERSION }}" + body: | + Updates the release version to ${{ env.VERSION }}. + branch: "release/${{ env.VERSION }}" + base: "master" + + - name: Post to a Slack channel + id: slack + uses: slackapi/slack-github-action@v1.25.0 + with: + channel-id: 'C06RXV161RA' + payload: | + { + "text": "iOS Release Branch Notification", + "blocks": [ + { + "type": "header", + "text": { + "type": "plain_text", + "text": "🚀 iOS Release Branch Created" + } + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "*Branch Name:* *`release/${{ env.VERSION }}`*\n*Status:* Success :white_check_mark:" + } + }, + { + "type": "divider" + }, + { + "type": "actions", + "elements": [ + { + "type": "button", + "text": { + "type": "plain_text", + "text": "Pull Request" + }, + "url": "${{ steps.create-pr.outputs.pull-request-url }}" + } + ] + } + ] + } + env: + SLACK_BOT_TOKEN: ${{ secrets.SLACK_MOBILE_NOTIFIER_TOKEN }} \ No newline at end of file diff --git a/git-release-branch-create.sh b/git-release-branch-create.sh index 95a42a70..1ff343f4 100755 --- a/git-release-branch-create.sh +++ b/git-release-branch-create.sh @@ -52,4 +52,9 @@ git add $notifivation_podspec_file git add $sdkversionprovider_file git commit -m "Bump SDK version to $version" -echo "Branch $branch_name has been created." +git push origin $branch_name + +git tag $branch_name +git push origin $branch_name --tags + +echo "Branch $branch_name has been created and pushed." From 292b87c246a8ecd631be5de776668346987215fa Mon Sep 17 00:00:00 2001 From: Sergei Semko <28645140+justSmK@users.noreply.github.com> Date: Mon, 1 Apr 2024 17:38:14 +0300 Subject: [PATCH 02/35] GitHub actions workflow setup update (#322) --- .github/workflows/build.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1097129b..f8ece5f3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -20,6 +20,10 @@ jobs: run: gem install bundler - name: Install bundler dependencies run: bundle install + - name: Install yeetd + run: | + wget https://github.com/biscuitehh/yeetd/releases/download/1.0/yeetd-normal.pkg + sudo installer -pkg yeetd-normal.pkg -target / + yeetd & - name: Run unit tests run: bundle exec fastlane unitTestLane - From 9a7462c6385d649f52acc5a0bce5c54d6e392e31 Mon Sep 17 00:00:00 2001 From: Akylbek Utekeshev Date: Tue, 2 Apr 2024 02:43:16 +0500 Subject: [PATCH 03/35] MBX-3264 Bug targeting primary logic --- .../InAppConfigutationMapper.swift | 54 ++++++++++++------- Mindbox/Info.plist | 2 +- 2 files changed, 36 insertions(+), 20 deletions(-) diff --git a/Mindbox/InAppMessages/InAppConfigurationMapper/InAppConfigutationMapper.swift b/Mindbox/InAppMessages/InAppConfigurationMapper/InAppConfigutationMapper.swift index 2cd66653..63759c4a 100644 --- a/Mindbox/InAppMessages/InAppConfigurationMapper/InAppConfigutationMapper.swift +++ b/Mindbox/InAppMessages/InAppConfigurationMapper/InAppConfigutationMapper.swift @@ -21,6 +21,7 @@ final class InAppConfigutationMapper: InAppConfigurationMapperProtocol { var targetingChecker: InAppTargetingCheckerProtocol private let persistenceStorage: PersistenceStorage var filteredInAppsByEvent: [InAppMessageTriggerEvent: [InAppTransitionData]] = [:] + var filteredInappsByEventForTargeting: [InAppMessageTriggerEvent: [InAppTransitionData]] = [:] private let sdkVersionValidator: SDKVersionValidator private let urlExtractorService: VariantImageUrlExtractorServiceProtocol private let abTestDeviceMixer: ABTestDeviceMixer @@ -28,10 +29,8 @@ final class InAppConfigutationMapper: InAppConfigurationMapperProtocol { let dataFacade: InAppConfigurationDataFacadeProtocol private var fullListOfInapps: [InApp] = [] - private var inappsDictForTargeting: [InAppMessageTriggerEvent: [InAppTransitionData]] = [:] private var savedEventForTargeting: ApplicationEvent? - private var shownInnapId: String = "" - private var completionSuccess = false + private var shownInnapId = "" init(inappFilterService: InappFilterProtocol, targetingChecker: InAppTargetingCheckerProtocol, @@ -58,18 +57,22 @@ final class InAppConfigutationMapper: InAppConfigurationMapperProtocol { self.targetingChecker.event = nil fullListOfInapps = inappFilterService.filter(inapps: response.inapps?.elements) let responseInapps = filterInappsByABTests(response.abtests, responseInapps: fullListOfInapps) - let filteredInapps = filterInappsBySDKVersion(responseInapps, shownInAppsIds: shownInAppsIds) + let filteredInapps = filterInappsByAlreadyShown(responseInapps, shownInAppsIds: shownInAppsIds) Logger.common(message: "Shown in-apps ids: [\(shownInAppsIds)]", level: .info, category: .inAppMessages) targetingChecker.event = event prepareTargetingChecker(for: filteredInapps) dataFacade.setObservedOperation() + prepareForRemainingTargeting() + if filteredInapps.isEmpty { Logger.common(message: "No inapps to show", level: .debug, category: .inAppMessages) completion(nil) return } + + dataFacade.fetchDependencies(model: event?.model) { self.filterByInappsEvents(inapps: filteredInapps, filteredInAppsByEvent: &self.filteredInAppsByEvent) @@ -92,26 +95,40 @@ final class InAppConfigutationMapper: InAppConfigurationMapperProtocol { } } + func prepareForRemainingTargeting() { + let estimatedInapps = fullListOfInapps + prepareTargetingChecker(for: estimatedInapps) + + self.dataFacade.fetchDependencies(model: savedEventForTargeting?.model) { + self.filterByInappsEvents(inapps: estimatedInapps, + filteredInAppsByEvent: &self.filteredInappsByEventForTargeting) + } + } + func sendRemainingInappsTargeting() { Logger.common(message: "TR | Initiating processing of remaining in-app targeting requests.", level: .debug, category: .inAppMessages) Logger.common(message: "TR | Full list of in-app messages: \(fullListOfInapps.map { $0.id })", level: .debug, category: .inAppMessages) Logger.common(message: "TR | Saved event for targeting: \(savedEventForTargeting?.name ?? "None")", level: .debug, category: .inAppMessages) + + var inappsForTargeting = self.inAppsByEventForTargeting(event: self.savedEventForTargeting, + asd: self.filteredInappsByEventForTargeting) - self.prepareTargetingChecker(for: fullListOfInapps) - dataFacade.setObservedOperation() - self.dataFacade.fetchDependencies(model: savedEventForTargeting?.model) { - self.filterByInappsEvents(inapps: self.fullListOfInapps, filteredInAppsByEvent: &self.inappsDictForTargeting) - let inappsForTargeting = self.inAppsByEventForTargeting(event: self.savedEventForTargeting, asd: self.inappsDictForTargeting) - var ids = inappsForTargeting.map { $0.inAppId } - if self.completionSuccess && ids.contains(self.shownInnapId) { - ids.removeAll { $0 == self.shownInnapId } + Logger.common(message: "TR | In-apps selected for targeting requests: \(inappsForTargeting)", level: .debug, category: .inAppMessages) + + var estimatedInapps = fullListOfInapps + estimatedInapps.removeAll(where: { + $0.id == shownInnapId + }) + + var asd = inappsForTargeting.filter { inapp in + estimatedInapps.contains { estimatedInapp in + inapp.inAppId == estimatedInapp.id } - - let setIds = Set(ids) - Logger.common(message: "TR | In-apps selected for targeting requests: \(setIds)", level: .debug, category: .inAppMessages) - setIds.forEach { self.dataFacade.trackTargeting(id: $0) } - self.completionSuccess = false } + + asd.forEach( { + self.dataFacade.trackTargeting(id: $0.inAppId) + }) } func inAppsByEventForTargeting(event: ApplicationEvent?, asd: [InAppMessageTriggerEvent: [InAppTransitionData]]) -> [InAppTransitionData] { @@ -129,7 +146,7 @@ final class InAppConfigutationMapper: InAppConfigurationMapperProtocol { } } - func filterInappsBySDKVersion(_ inapps: [InApp]?, shownInAppsIds: Set) -> [InApp] { + func filterInappsByAlreadyShown(_ inapps: [InApp]?, shownInAppsIds: Set) -> [InApp] { guard let inapps = inapps else { return [] } @@ -301,7 +318,6 @@ final class InAppConfigutationMapper: InAppConfigurationMapperProtocol { DispatchQueue.main.async { [weak self] in self?.shownInnapId = formData?.inAppId ?? "" self?.dataFacade.trackTargeting(id: formData?.inAppId) - self?.completionSuccess = true completion(formData) } } diff --git a/Mindbox/Info.plist b/Mindbox/Info.plist index 2983809b..d1d38d44 100644 --- a/Mindbox/Info.plist +++ b/Mindbox/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion - 5293 + 5339 From ec55da10b1dc5246b3747c6850ad22a5e26a1764 Mon Sep 17 00:00:00 2001 From: Akylbek Utekeshev Date: Tue, 2 Apr 2024 04:34:36 +0500 Subject: [PATCH 04/35] MBX-3264 Test. Need check --- Mindbox/DI/DependencyProvider.swift | 10 +- .../InAppConfigutationMapper.swift | 166 ++++-------------- .../InappFilterService/InappFilter.swift | 123 +++++++++++-- Mindbox/Info.plist | 2 +- MindboxTests/DI/TestDependencyProvider.swift | 5 +- .../InAppTargetingRequestsTests.swift | 7 +- .../InappFilterServiceTests.swift | 46 ++--- 7 files changed, 178 insertions(+), 181 deletions(-) diff --git a/Mindbox/DI/DependencyProvider.swift b/Mindbox/DI/DependencyProvider.swift index be60dd33..0662b0b1 100644 --- a/Mindbox/DI/DependencyProvider.swift +++ b/Mindbox/DI/DependencyProvider.swift @@ -81,7 +81,12 @@ final class DependencyProvider: DependencyContainer { let variantsFilterService = VariantFilterService(layersFilter: layersFilterService, elementsFilter: elementsFilterService, contentPositionFilter: contentPositionFilterService) - inappFilterService = InappsFilterService(variantsFilter: variantsFilterService) + + inappFilterService = InappsFilterService(persistenceStorage: persistenceStorage, + abTestDeviceMixer: abTestDeviceMixer, + variantsFilter: variantsFilterService, + sdkVersionValidator: sdkVersionValidator) + inAppConfigurationDataFacade = InAppConfigurationDataFacade(geoService: geoService, segmentationService: segmentationSevice, targetingChecker: inAppTargetingChecker, @@ -94,10 +99,7 @@ final class DependencyProvider: DependencyContainer { inAppConfigRepository: InAppConfigurationRepository(), inAppConfigurationMapper: InAppConfigutationMapper(inappFilterService: inappFilterService, targetingChecker: inAppTargetingChecker, - persistenceStorage: persistenceStorage, - sdkVersionValidator: sdkVersionValidator, urlExtractorService: urlExtractorService, - abTestDeviceMixer: abTestDeviceMixer, dataFacade: inAppConfigurationDataFacade), logsManager: logsManager), presentationManager: presentationManager, diff --git a/Mindbox/InAppMessages/InAppConfigurationMapper/InAppConfigutationMapper.swift b/Mindbox/InAppMessages/InAppConfigurationMapper/InAppConfigutationMapper.swift index 63759c4a..2433cb09 100644 --- a/Mindbox/InAppMessages/InAppConfigurationMapper/InAppConfigutationMapper.swift +++ b/Mindbox/InAppMessages/InAppConfigurationMapper/InAppConfigutationMapper.swift @@ -17,34 +17,26 @@ protocol InAppConfigurationMapperProtocol { final class InAppConfigutationMapper: InAppConfigurationMapperProtocol { - private let inappFilterService: InappFilterProtocol var targetingChecker: InAppTargetingCheckerProtocol - private let persistenceStorage: PersistenceStorage var filteredInAppsByEvent: [InAppMessageTriggerEvent: [InAppTransitionData]] = [:] var filteredInappsByEventForTargeting: [InAppMessageTriggerEvent: [InAppTransitionData]] = [:] - private let sdkVersionValidator: SDKVersionValidator - private let urlExtractorService: VariantImageUrlExtractorServiceProtocol - private let abTestDeviceMixer: ABTestDeviceMixer let dataFacade: InAppConfigurationDataFacadeProtocol - private var fullListOfInapps: [InApp] = [] + private let inappFilterService: InappFilterProtocol + private let urlExtractorService: VariantImageUrlExtractorServiceProtocol + + private var validInapps: [InApp] = [] private var savedEventForTargeting: ApplicationEvent? private var shownInnapId = "" init(inappFilterService: InappFilterProtocol, targetingChecker: InAppTargetingCheckerProtocol, - persistenceStorage: PersistenceStorage, - sdkVersionValidator: SDKVersionValidator, urlExtractorService: VariantImageUrlExtractorServiceProtocol, - abTestDeviceMixer: ABTestDeviceMixer, dataFacade: InAppConfigurationDataFacadeProtocol) { self.inappFilterService = inappFilterService self.targetingChecker = targetingChecker - self.persistenceStorage = persistenceStorage - self.sdkVersionValidator = sdkVersionValidator self.urlExtractorService = urlExtractorService - self.abTestDeviceMixer = abTestDeviceMixer self.dataFacade = dataFacade } @@ -52,13 +44,11 @@ final class InAppConfigutationMapper: InAppConfigurationMapperProtocol { func mapConfigResponse(_ event: ApplicationEvent?, _ response: ConfigResponse, _ completion: @escaping (InAppFormData?) -> Void) { - let shownInAppsIds = Set(persistenceStorage.shownInAppsIds ?? []) savedEventForTargeting = event self.targetingChecker.event = nil - fullListOfInapps = inappFilterService.filter(inapps: response.inapps?.elements) - let responseInapps = filterInappsByABTests(response.abtests, responseInapps: fullListOfInapps) - let filteredInapps = filterInappsByAlreadyShown(responseInapps, shownInAppsIds: shownInAppsIds) - Logger.common(message: "Shown in-apps ids: [\(shownInAppsIds)]", level: .info, category: .inAppMessages) + + let filteredInapps = inappFilterService.filter(inapps: response.inapps?.elements, abTests: response.abtests) + validInapps = inappFilterService.validInapps targetingChecker.event = event prepareTargetingChecker(for: filteredInapps) @@ -71,8 +61,6 @@ final class InAppConfigutationMapper: InAppConfigurationMapperProtocol { completion(nil) return } - - dataFacade.fetchDependencies(model: event?.model) { self.filterByInappsEvents(inapps: filteredInapps, filteredInAppsByEvent: &self.filteredInAppsByEvent) @@ -96,7 +84,7 @@ final class InAppConfigutationMapper: InAppConfigurationMapperProtocol { } func prepareForRemainingTargeting() { - let estimatedInapps = fullListOfInapps + let estimatedInapps = validInapps prepareTargetingChecker(for: estimatedInapps) self.dataFacade.fetchDependencies(model: savedEventForTargeting?.model) { @@ -106,125 +94,28 @@ final class InAppConfigutationMapper: InAppConfigurationMapperProtocol { } func sendRemainingInappsTargeting() { - Logger.common(message: "TR | Initiating processing of remaining in-app targeting requests.", level: .debug, category: .inAppMessages) - Logger.common(message: "TR | Full list of in-app messages: \(fullListOfInapps.map { $0.id })", level: .debug, category: .inAppMessages) - Logger.common(message: "TR | Saved event for targeting: \(savedEventForTargeting?.name ?? "None")", level: .debug, category: .inAppMessages) - - var inappsForTargeting = self.inAppsByEventForTargeting(event: self.savedEventForTargeting, - asd: self.filteredInappsByEventForTargeting) + let logMessage = """ + TR | Initiating processing of remaining in-app targeting requests. + Full list of in-app messages: \(validInapps.map { $0.id }) + Saved event for targeting: \(savedEventForTargeting?.name ?? "None") + """ + Logger.common(message: logMessage, level: .debug, category: .inAppMessages) - Logger.common(message: "TR | In-apps selected for targeting requests: \(inappsForTargeting)", level: .debug, category: .inAppMessages) - - var estimatedInapps = fullListOfInapps - estimatedInapps.removeAll(where: { - $0.id == shownInnapId - }) + let targetedEventKey: InAppMessageTriggerEvent = savedEventForTargeting != nil + ? .applicationEvent(savedEventForTargeting!) + : .start - var asd = inappsForTargeting.filter { inapp in - estimatedInapps.contains { estimatedInapp in - inapp.inAppId == estimatedInapp.id - } - } + let targetedInApps = filteredInappsByEventForTargeting[targetedEventKey]?.filter { inAppData in + !validInapps.contains { $0.id == shownInnapId && $0.id == inAppData.inAppId } + } ?? [] + + Logger.common(message: "TR | In-apps selected for targeting requests: \(targetedInApps.map { $0.inAppId })", level: .debug, category: .inAppMessages) - asd.forEach( { - self.dataFacade.trackTargeting(id: $0.inAppId) - }) - } - - func inAppsByEventForTargeting(event: ApplicationEvent?, asd: [InAppMessageTriggerEvent: [InAppTransitionData]]) -> [InAppTransitionData] { - if let event = event { - if let inappsByEvent = asd[.applicationEvent(event)] { - return inappsByEvent - } else { - return [] - } - } else if let inappsByEvent = asd[.start] { - return inappsByEvent - } else { - Logger.common(message: "No inapps available for the event or start.") - return [] - } - } - - func filterInappsByAlreadyShown(_ inapps: [InApp]?, shownInAppsIds: Set) -> [InApp] { - guard let inapps = inapps else { - return [] - } - - let filteredInapps = inapps.filter { - sdkVersionValidator.isValid(item: $0.sdkVersion) - && !shownInAppsIds.contains($0.id) + targetedInApps.forEach { inAppData in + dataFacade.trackTargeting(id: inAppData.inAppId) } - return filteredInapps - } - - func filterInappsByABTests(_ abTests: [ABTest]?, responseInapps: [InApp]?) -> [InApp] { - let responseInapps = responseInapps ?? [] - guard let abTests = abTests, !abTests.isEmpty else { - return responseInapps - } - - var result: [InApp] = responseInapps - - for abTest in abTests { - guard let uuid = UUID(uuidString: persistenceStorage.deviceUUID ?? "" ), - let salt = abTest.salt, - let variants = abTest.variants else { - continue - } - - let hashValue = try? abTestDeviceMixer.modulusGuidHash(identifier: uuid, salt: salt) - - guard let hashValue = hashValue else { - continue - } - - Logger.common(message: "[Hash Value]: \(hashValue) for [UUID]: \(persistenceStorage.deviceUUID ?? "nil")") - Logger.common(message: "[AB-test ID]: \(abTest.id)") - - var allInappsInVariantsExceptCurrentBranch: [String] = [] - - for variant in variants { - if let objects = variant.objects { - for object in objects { - if object.kind == .all { - responseInapps.forEach( { - allInappsInVariantsExceptCurrentBranch.append($0.id) - }) - } else { - allInappsInVariantsExceptCurrentBranch += object.inapps ?? [] - } - } - } - } - - var setInapps = Set(allInappsInVariantsExceptCurrentBranch) - - for variant in variants { - if let modulus = variant.modulus, let objects = variant.objects, let upper = modulus.upper { - let range = modulus.lower.. [InApp] + func filter(inapps: [InAppDTO]?, abTests: [ABTest]?) -> [InApp] + var validInapps: [InApp] { get } + var shownInAppsIds: Set { get } } final class InappsFilterService: InappFilterProtocol { + + var validInapps: [InApp] = [] + var shownInAppsIds: Set = [] + + private let persistenceStorage: PersistenceStorage + private let abTestDeviceMixer: ABTestDeviceMixer private let variantsFilter: VariantFilterProtocol + private let sdkVersionValidator: SDKVersionValidator - init(variantsFilter: VariantFilterProtocol) { + init(persistenceStorage: PersistenceStorage, abTestDeviceMixer: ABTestDeviceMixer, variantsFilter: VariantFilterProtocol, sdkVersionValidator: SDKVersionValidator) { + self.persistenceStorage = persistenceStorage + self.abTestDeviceMixer = abTestDeviceMixer self.variantsFilter = variantsFilter + self.sdkVersionValidator = sdkVersionValidator } - func filter(inapps: [InAppDTO]?) -> [InApp] { + func filter(inapps: [InAppDTO]?, abTests: [ABTest]?) -> [InApp] { guard var inapps = inapps else { Logger.common(message: "Received nil for in-apps. Returning an empty array.", level: .debug, category: .inAppMessages) return [] } inapps = filterInappsBySDKVersion(inapps) - Logger.common(message: "Processing \(inapps.count) in-app(s).", level: .debug, category: .inAppMessages) + let validInapps = filterValidInAppMessages(inapps) + let filteredByABTestInapps = filterInappsByABTests(abTests, responseInapps: validInapps) + let filteredByAlreadyShown = filterInappsByAlreadyShown(filteredByABTestInapps) + return filteredByAlreadyShown + } +} + +// MARK: - Private methods +private extension InappsFilterService { + func filterInappsBySDKVersion(_ inapps: [InAppDTO]) -> [InAppDTO] { + let inapps = inapps + + let filteredInapps = inapps.filter { + let minVersionValid = $0.sdkVersion.min.map { $0 <= Constants.Versions.sdkVersionNumeric } ?? false + let maxVersionValid = $0.sdkVersion.max.map { $0 >= Constants.Versions.sdkVersionNumeric } ?? true + + return minVersionValid && maxVersionValid + } + + return filteredInapps + } + + func filterValidInAppMessages(_ inapps: [InAppDTO]) -> [InApp] { var filteredInapps: [InApp] = [] for inapp in inapps { do { @@ -48,17 +82,84 @@ final class InappsFilterService: InappFilterProtocol { } Logger.common(message: "Filtering process completed. \(filteredInapps.count) valid in-app(s) found.", level: .debug, category: .inAppMessages) + validInapps = filteredInapps return filteredInapps } - func filterInappsBySDKVersion(_ inapps: [InAppDTO]) -> [InAppDTO] { - let inapps = inapps - - let filteredInapps = inapps.filter { - let minVersionValid = $0.sdkVersion.min.map { $0 <= Constants.Versions.sdkVersionNumeric } ?? false - let maxVersionValid = $0.sdkVersion.max.map { $0 >= Constants.Versions.sdkVersionNumeric } ?? true + func filterInappsByABTests(_ abTests: [ABTest]?, responseInapps: [InApp]?) -> [InApp] { + let responseInapps = responseInapps ?? [] + guard let abTests = abTests, !abTests.isEmpty else { + return responseInapps + } + + var result: [InApp] = responseInapps + + for abTest in abTests { + guard let uuid = UUID(uuidString: persistenceStorage.deviceUUID ?? "" ), + let salt = abTest.salt, + let variants = abTest.variants else { + continue + } - return minVersionValid && maxVersionValid + let hashValue = try? abTestDeviceMixer.modulusGuidHash(identifier: uuid, salt: salt) + + guard let hashValue = hashValue else { + continue + } + + Logger.common(message: "[Hash Value]: \(hashValue) for [UUID]: \(persistenceStorage.deviceUUID ?? "nil")") + Logger.common(message: "[AB-test ID]: \(abTest.id)") + + var allInappsInVariantsExceptCurrentBranch: [String] = [] + + for variant in variants { + if let objects = variant.objects { + for object in objects { + if object.kind == .all { + responseInapps.forEach( { + allInappsInVariantsExceptCurrentBranch.append($0.id) + }) + } else { + allInappsInVariantsExceptCurrentBranch += object.inapps ?? [] + } + } + } + } + + var setInapps = Set(allInappsInVariantsExceptCurrentBranch) + + for variant in variants { + if let modulus = variant.modulus, let objects = variant.objects, let upper = modulus.upper { + let range = modulus.lower.. [InApp] { + shownInAppsIds = Set(persistenceStorage.shownInAppsIds ?? []) + Logger.common(message: "Shown in-apps ids: [\(shownInAppsIds)]", level: .info, category: .inAppMessages) + let filteredInapps = inapps.filter { + sdkVersionValidator.isValid(item: $0.sdkVersion) + && !shownInAppsIds.contains($0.id) } return filteredInapps diff --git a/Mindbox/Info.plist b/Mindbox/Info.plist index d1d38d44..d34e6b0c 100644 --- a/Mindbox/Info.plist +++ b/Mindbox/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion - 5339 + 5349 diff --git a/MindboxTests/DI/TestDependencyProvider.swift b/MindboxTests/DI/TestDependencyProvider.swift index 38d33acd..fc491e7e 100644 --- a/MindboxTests/DI/TestDependencyProvider.swift +++ b/MindboxTests/DI/TestDependencyProvider.swift @@ -83,7 +83,10 @@ final class TestDependencyProvider: DependencyContainer { let variantsFilterService = VariantFilterService(layersFilter: layersFilterService, elementsFilter: elementsFilterService, contentPositionFilter: contentPositionFilterService) - inappFilterService = InappsFilterService(variantsFilter: variantsFilterService) + inappFilterService = InappsFilterService(persistenceStorage: persistenceStorage, + abTestDeviceMixer: abTestDeviceMixer, + variantsFilter: variantsFilterService, + sdkVersionValidator: sdkVersionValidator) pushValidator = MindboxPushValidator() userVisitManager = UserVisitManager(persistenceStorage: persistenceStorage, sessionManager: sessionManager) } diff --git a/MindboxTests/InApp/Tests/InAppConfigResponseTests/InAppTargetingRequestsTests.swift b/MindboxTests/InApp/Tests/InAppConfigResponseTests/InAppTargetingRequestsTests.swift index 22b72690..516fda27 100644 --- a/MindboxTests/InApp/Tests/InAppConfigResponseTests/InAppTargetingRequestsTests.swift +++ b/MindboxTests/InApp/Tests/InAppConfigResponseTests/InAppTargetingRequestsTests.swift @@ -34,10 +34,7 @@ class AInAppTargetingRequestsTests: XCTestCase { mapper = InAppConfigutationMapper(inappFilterService: container.inappFilterService, targetingChecker: targetingChecker, - persistenceStorage: container.persistenceStorage, - sdkVersionValidator: container.sdkVersionValidator, urlExtractorService: container.urlExtractorService, - abTestDeviceMixer: container.abTestDeviceMixer, dataFacade: mockDataFacade) } @@ -77,7 +74,7 @@ class AInAppTargetingRequestsTests: XCTestCase { } wait(for: [expectation], timeout: 1) - targetingContains("2", expectedToShow: true) + targetingContains("2") targetingContains("1") } catch { XCTFail("Some error: \(error)") @@ -142,7 +139,7 @@ class AInAppTargetingRequestsTests: XCTestCase { } wait(for: [expectationForsendRemainingInappsTargeting], timeout: 1) - targetingContains("3", expectedToShow: true) + targetingContains("3") targetingContains("1") targetingContains("4") targetingContains("5") diff --git a/MindboxTests/InApp/Tests/InAppConfigurationMapperTests/InappFilterServiceTests/InappFilterServiceTests.swift b/MindboxTests/InApp/Tests/InAppConfigurationMapperTests/InappFilterServiceTests/InappFilterServiceTests.swift index aedba78d..f477d04e 100644 --- a/MindboxTests/InApp/Tests/InAppConfigurationMapperTests/InappFilterServiceTests/InappFilterServiceTests.swift +++ b/MindboxTests/InApp/Tests/InAppConfigurationMapperTests/InappFilterServiceTests/InappFilterServiceTests.swift @@ -34,35 +34,35 @@ final class InappFilterServiceTests: XCTestCase { func test_unknown_type_for_variants() throws { let config = try getConfig(name: "unknownVariantType") - let inapps = sut.filter(inapps: config.inapps?.elements) + let inapps = sut.filter(inapps: config.inapps?.elements, abTests: config.abtests) XCTAssertEqual(inapps.count, 1) XCTAssertEqual(inapps.first?.id, Constants.defaultID) } func test_missingBackgroundSection() throws { let config = try getConfig(name: "missingBackgroundSection") - let inapps = sut.filter(inapps: config.inapps?.elements) + let inapps = sut.filter(inapps: config.inapps?.elements, abTests: config.abtests) XCTAssertEqual(inapps.count, 1) XCTAssertEqual(inapps.first?.id, Constants.defaultID) } func test_emptyLayersSection() throws { let config = try getConfig(name: "emptyLayersSection") - let inapps = sut.filter(inapps: config.inapps?.elements) + let inapps = sut.filter(inapps: config.inapps?.elements, abTests: config.abtests) XCTAssertEqual(inapps.count, 1) XCTAssertEqual(inapps.first?.id, Constants.defaultID) } func test_unknownLayerType() throws { let config = try getConfig(name: "unknownLayerType") - let inapps = sut.filter(inapps: config.inapps?.elements) + let inapps = sut.filter(inapps: config.inapps?.elements, abTests: config.abtests) XCTAssertEqual(inapps.count, 1) XCTAssertEqual(inapps.first?.id, Constants.defaultID) } func test_knownImageUnknownPictureLayerType() throws { let config = try getConfig(name: "knownImageUnknownPictureLayerType") - let inapps = sut.filter(inapps: config.inapps?.elements) + let inapps = sut.filter(inapps: config.inapps?.elements, abTests: config.abtests) XCTAssertEqual(inapps.count, 2) if let variant = inapps.first?.form.variants.first { @@ -78,63 +78,63 @@ final class InappFilterServiceTests: XCTestCase { func test_unknownActionLayerType() throws { let config = try getConfig(name: "unknownActionLayerType") - let inapps = sut.filter(inapps: config.inapps?.elements) + let inapps = sut.filter(inapps: config.inapps?.elements, abTests: config.abtests) XCTAssertEqual(inapps.count, 1) XCTAssertEqual(inapps.first?.id, Constants.defaultID) } func test_redirectUrlValueNumberInsteadOfString() throws { let config = try getConfig(name: "redirectUrlValueNumberInsteadOfString") - let inapps = sut.filter(inapps: config.inapps?.elements) + let inapps = sut.filter(inapps: config.inapps?.elements, abTests: config.abtests) XCTAssertEqual(inapps.count, 1) XCTAssertEqual(inapps.first?.id, Constants.defaultID) } func test_missingIntentPayloadInActionLayer() throws { let config = try getConfig(name: "missingIntentPayloadInActionLayer") - let inapps = sut.filter(inapps: config.inapps?.elements) + let inapps = sut.filter(inapps: config.inapps?.elements, abTests: config.abtests) XCTAssertEqual(inapps.count, 1) XCTAssertEqual(inapps.first?.id, Constants.defaultID) } func test_missingSourceSection() throws { let config = try getConfig(name: "missingSourceSection") - let inapps = sut.filter(inapps: config.inapps?.elements) + let inapps = sut.filter(inapps: config.inapps?.elements, abTests: config.abtests) XCTAssertEqual(inapps.count, 1) XCTAssertEqual(inapps.first?.id, Constants.defaultID) } func test_emptyVariantsArray() throws { let config = try getConfig(name: "emptyVariantsArray") - let inapps = sut.filter(inapps: config.inapps?.elements) + let inapps = sut.filter(inapps: config.inapps?.elements, abTests: config.abtests) XCTAssertEqual(inapps.count, 1) XCTAssertEqual(inapps.first?.id, Constants.defaultID) } func test_unknownSourceType() throws { let config = try getConfig(name: "unknownSourceType") - let inapps = sut.filter(inapps: config.inapps?.elements) + let inapps = sut.filter(inapps: config.inapps?.elements, abTests: config.abtests) XCTAssertEqual(inapps.count, 1) XCTAssertEqual(inapps.first?.id, Constants.defaultID) } func test_missingValueInSourceLayer() throws { let config = try getConfig(name: "missingValueInSourceLayer") - let inapps = sut.filter(inapps: config.inapps?.elements) + let inapps = sut.filter(inapps: config.inapps?.elements, abTests: config.abtests) XCTAssertEqual(inapps.count, 1) XCTAssertEqual(inapps.first?.id, Constants.defaultID) } func test_missingImageLinkInSourceLayerValue() throws { let config = try getConfig(name: "missingImageLinkInSourceLayerValue") - let inapps = sut.filter(inapps: config.inapps?.elements) + let inapps = sut.filter(inapps: config.inapps?.elements, abTests: config.abtests) XCTAssertEqual(inapps.count, 1) XCTAssertEqual(inapps.first?.id, Constants.defaultID) } func test_missingElementsSection() throws { let config = try getConfig(name: "missingElementsSection") - let inapps = sut.filter(inapps: config.inapps?.elements) + let inapps = sut.filter(inapps: config.inapps?.elements, abTests: config.abtests) XCTAssertEqual(inapps.count, 2) if let variant = inapps.first?.form.variants.first { @@ -150,7 +150,7 @@ final class InappFilterServiceTests: XCTestCase { func test_invalidCloseButtonColor() throws { let config = try getConfig(name: "invalidCloseButtonColor") - let inapps = sut.filter(inapps: config.inapps?.elements) + let inapps = sut.filter(inapps: config.inapps?.elements, abTests: config.abtests) XCTAssertEqual(inapps.count, 2) if let variant = inapps.first?.form.variants.first { @@ -174,7 +174,7 @@ final class InappFilterServiceTests: XCTestCase { func test_missingCloseButtonColorLineWidthSize() throws { let config = try getConfig(name: "missingCloseButtonColorLineWidthSize") - let inapps = sut.filter(inapps: config.inapps?.elements) + let inapps = sut.filter(inapps: config.inapps?.elements, abTests: config.abtests) XCTAssertEqual(inapps.count, 2) if let variant = inapps.first?.form.variants.first { @@ -201,7 +201,7 @@ final class InappFilterServiceTests: XCTestCase { func test_twoCloseButtonsInApp() throws { let config = try getConfig(name: "twoCloseButtonsInApp") - let inapps = sut.filter(inapps: config.inapps?.elements) + let inapps = sut.filter(inapps: config.inapps?.elements, abTests: config.abtests) XCTAssertEqual(inapps.count, 2) if let variant = inapps.first?.form.variants.first { @@ -226,7 +226,7 @@ final class InappFilterServiceTests: XCTestCase { func test_closeButtonWithOpenButton() throws { let config = try getConfig(name: "closeButtonWithOpenButton") - let inapps = sut.filter(inapps: config.inapps?.elements) + let inapps = sut.filter(inapps: config.inapps?.elements, abTests: config.abtests) XCTAssertEqual(inapps.count, 2) if let variant = inapps.first?.form.variants.first { @@ -255,7 +255,7 @@ final class InappFilterServiceTests: XCTestCase { func test_unknownSizeKind() throws { let config = try getConfig(name: "unknownSizeKind") - let inapps = sut.filter(inapps: config.inapps?.elements) + let inapps = sut.filter(inapps: config.inapps?.elements, abTests: config.abtests) XCTAssertEqual(inapps.count, 2) if let variant = inapps.first?.form.variants.first { @@ -286,28 +286,28 @@ final class InappFilterServiceTests: XCTestCase { func test_missingMarginFieldInSection() throws { let config = try getConfig(name: "missingMarginFieldInSection") - let inapps = sut.filter(inapps: config.inapps?.elements) + let inapps = sut.filter(inapps: config.inapps?.elements, abTests: config.abtests) XCTAssertEqual(inapps.count, 1) XCTAssertEqual(inapps.first?.id, Constants.defaultID) } func test_negativeCloseButtonSizeValues() throws { let config = try getConfig(name: "negativeCloseButtonSizeValues") - let inapps = sut.filter(inapps: config.inapps?.elements) + let inapps = sut.filter(inapps: config.inapps?.elements, abTests: config.abtests) XCTAssertEqual(inapps.count, 1) XCTAssertEqual(inapps.first?.id, Constants.defaultID) } func test_closeButtonMarginAboveOne() throws { let config = try getConfig(name: "closeButtonMarginAboveOne") - let inapps = sut.filter(inapps: config.inapps?.elements) + let inapps = sut.filter(inapps: config.inapps?.elements, abTests: config.abtests) XCTAssertEqual(inapps.count, 1) XCTAssertEqual(inapps.first?.id, Constants.defaultID) } func test_closeButtonMarginBelowZero() throws { let config = try getConfig(name: "closeButtonMarginBelowZero") - let inapps = sut.filter(inapps: config.inapps?.elements) + let inapps = sut.filter(inapps: config.inapps?.elements, abTests: config.abtests) XCTAssertEqual(inapps.count, 1) XCTAssertEqual(inapps.first?.id, Constants.defaultID) } From 239bdfe58da3d41ce18159f20312fa20bf40bbd7 Mon Sep 17 00:00:00 2001 From: Akylbek Utekeshev Date: Wed, 3 Apr 2024 13:30:38 +0500 Subject: [PATCH 05/35] MBX-3264 Change way to check pushEnabled --- .../InAppConfigutationMapper.swift | 10 ++++---- .../PushEnabledTargetingChecker.swift | 25 ++++++++++++------- Mindbox/Info.plist | 2 +- .../InAppTargetingRequestsTests.swift | 4 +-- 4 files changed, 24 insertions(+), 17 deletions(-) diff --git a/Mindbox/InAppMessages/InAppConfigurationMapper/InAppConfigutationMapper.swift b/Mindbox/InAppMessages/InAppConfigurationMapper/InAppConfigutationMapper.swift index 2433cb09..13a23dca 100644 --- a/Mindbox/InAppMessages/InAppConfigurationMapper/InAppConfigutationMapper.swift +++ b/Mindbox/InAppMessages/InAppConfigurationMapper/InAppConfigutationMapper.swift @@ -86,14 +86,14 @@ final class InAppConfigutationMapper: InAppConfigurationMapperProtocol { func prepareForRemainingTargeting() { let estimatedInapps = validInapps prepareTargetingChecker(for: estimatedInapps) - - self.dataFacade.fetchDependencies(model: savedEventForTargeting?.model) { - self.filterByInappsEvents(inapps: estimatedInapps, - filteredInAppsByEvent: &self.filteredInappsByEventForTargeting) - } } func sendRemainingInappsTargeting() { + self.dataFacade.fetchDependencies(model: savedEventForTargeting?.model) { + self.filterByInappsEvents(inapps: self.validInapps, + filteredInAppsByEvent: &self.filteredInappsByEventForTargeting) + } + let logMessage = """ TR | Initiating processing of remaining in-app targeting requests. Full list of in-app messages: \(validInapps.map { $0.id }) diff --git a/Mindbox/InAppMessages/InAppTargetingChecker/TargetingCheckerTypes/PushEnabledTargetingChecker.swift b/Mindbox/InAppMessages/InAppTargetingChecker/TargetingCheckerTypes/PushEnabledTargetingChecker.swift index 69256924..6ca555f1 100644 --- a/Mindbox/InAppMessages/InAppTargetingChecker/TargetingCheckerTypes/PushEnabledTargetingChecker.swift +++ b/Mindbox/InAppMessages/InAppTargetingChecker/TargetingCheckerTypes/PushEnabledTargetingChecker.swift @@ -7,19 +7,26 @@ // import Foundation +import UserNotifications final class PushEnabledTargetingChecker: InternalTargetingChecker { override func checkInternal(targeting: PushEnabledTargeting) -> Bool { - var pushPermissionBoolean = true - switch SessionTemporaryStorage.shared.pushPermissionStatus { - case .notDetermined, .denied: - pushPermissionBoolean = false - case .authorized, .provisional, .ephemeral: - pushPermissionBoolean = true - @unknown default: - pushPermissionBoolean = true + let lock = DispatchSemaphore(value: 0) + var isNotificationsEnabled = true + + UNUserNotificationCenter.current().getNotificationSettings { settings in + switch settings.authorizationStatus { + case .notDetermined, .denied: + isNotificationsEnabled = false + case .authorized, .provisional, .ephemeral: + isNotificationsEnabled = true + @unknown default: + isNotificationsEnabled = true + } + lock.signal() } - return targeting.value == pushPermissionBoolean + lock.wait() + return targeting.value == isNotificationsEnabled } } diff --git a/Mindbox/Info.plist b/Mindbox/Info.plist index d34e6b0c..2c85542f 100644 --- a/Mindbox/Info.plist +++ b/Mindbox/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion - 5349 + 5351 diff --git a/MindboxTests/InApp/Tests/InAppConfigResponseTests/InAppTargetingRequestsTests.swift b/MindboxTests/InApp/Tests/InAppConfigResponseTests/InAppTargetingRequestsTests.swift index 516fda27..6989ddfd 100644 --- a/MindboxTests/InApp/Tests/InAppConfigResponseTests/InAppTargetingRequestsTests.swift +++ b/MindboxTests/InApp/Tests/InAppConfigResponseTests/InAppTargetingRequestsTests.swift @@ -9,7 +9,7 @@ import XCTest @testable import Mindbox -class AInAppTargetingRequestsTests: XCTestCase { +class InAppTargetingRequestsTests: XCTestCase { var container: TestDependencyProvider! @@ -380,7 +380,7 @@ class AInAppTargetingRequestsTests: XCTestCase { } private func getConfig(name: String) throws -> ConfigResponse { - let bundle = Bundle(for: AInAppTargetingRequestsTests.self) + let bundle = Bundle(for: InAppTargetingRequestsTests.self) let fileURL = bundle.url(forResource: name, withExtension: "json")! let data = try Data(contentsOf: fileURL) return try JSONDecoder().decode(ConfigResponse.self, from: data) From 44078d6c3ad7590c069441b493337f8f350c1740 Mon Sep 17 00:00:00 2001 From: Akylbek Utekeshev Date: Thu, 4 Apr 2024 02:16:25 +0500 Subject: [PATCH 06/35] MBX-3264 Updated logic --- .../InAppConfigutationMapper.swift | 53 +++++++++++-------- 1 file changed, 31 insertions(+), 22 deletions(-) diff --git a/Mindbox/InAppMessages/InAppConfigurationMapper/InAppConfigutationMapper.swift b/Mindbox/InAppMessages/InAppConfigurationMapper/InAppConfigutationMapper.swift index 13a23dca..70d1a7d5 100644 --- a/Mindbox/InAppMessages/InAppConfigurationMapper/InAppConfigutationMapper.swift +++ b/Mindbox/InAppMessages/InAppConfigurationMapper/InAppConfigutationMapper.swift @@ -92,30 +92,39 @@ final class InAppConfigutationMapper: InAppConfigurationMapperProtocol { self.dataFacade.fetchDependencies(model: savedEventForTargeting?.model) { self.filterByInappsEvents(inapps: self.validInapps, filteredInAppsByEvent: &self.filteredInappsByEventForTargeting) - } - - let logMessage = """ - TR | Initiating processing of remaining in-app targeting requests. - Full list of in-app messages: \(validInapps.map { $0.id }) - Saved event for targeting: \(savedEventForTargeting?.name ?? "None") - """ - Logger.common(message: logMessage, level: .debug, category: .inAppMessages) - - let targetedEventKey: InAppMessageTriggerEvent = savedEventForTargeting != nil - ? .applicationEvent(savedEventForTargeting!) + + let logMessage = """ + TR | Initiating processing of remaining in-app targeting requests. + Full list of in-app messages: \(self.validInapps.map { $0.id }) + Saved event for targeting: \(self.savedEventForTargeting?.name ?? "None") + """ + Logger.common(message: logMessage, level: .debug, category: .inAppMessages) + + let targetedEventKey: InAppMessageTriggerEvent = self.savedEventForTargeting != nil + ? .applicationEvent(self.savedEventForTargeting!) : .start - - let targetedInApps = filteredInappsByEventForTargeting[targetedEventKey]?.filter { inAppData in - !validInapps.contains { $0.id == shownInnapId && $0.id == inAppData.inAppId } - } ?? [] - - Logger.common(message: "TR | In-apps selected for targeting requests: \(targetedInApps.map { $0.inAppId })", level: .debug, category: .inAppMessages) + + guard let inappsByEvent = self.filteredInappsByEventForTargeting[targetedEventKey] else { + return + } + + let preparedForTrackTargetingInapps: Set = Set(self.validInapps.compactMap { inapp -> String? in + guard inapp.id != self.shownInnapId, + inappsByEvent.contains(where: { $0.inAppId == inapp.id }), + self.targetingChecker.check(targeting: inapp.targeting) else { + return nil + } + return inapp.id + }) + + Logger.common(message: "TR | In-apps selected for targeting requests: \(preparedForTrackTargetingInapps)", level: .debug, category: .inAppMessages) - targetedInApps.forEach { inAppData in - dataFacade.trackTargeting(id: inAppData.inAppId) + preparedForTrackTargetingInapps.forEach { id in + self.dataFacade.trackTargeting(id: id) + } + + self.shownInnapId = "" } - - shownInnapId = "" } private func prepareTargetingChecker(for inapps: [InApp]) { @@ -207,8 +216,8 @@ final class InAppConfigutationMapper: InAppConfigurationMapperProtocol { group.notify(queue: .main) { DispatchQueue.main.async { [weak self] in - self?.shownInnapId = formData?.inAppId ?? "" if !SessionTemporaryStorage.shared.isPresentingInAppMessage { + self?.shownInnapId = formData?.inAppId ?? "" self?.dataFacade.trackTargeting(id: formData?.inAppId) } From 79ac38c6db9bf2f22d9b29f9c8984ba5081287dd Mon Sep 17 00:00:00 2001 From: Akylbek Utekeshev Date: Thu, 4 Apr 2024 13:00:11 +0500 Subject: [PATCH 07/35] MBX-0000 Update MindboxLogger tag name --- MindboxLogger.podspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MindboxLogger.podspec b/MindboxLogger.podspec index f26ca741..1d9b55eb 100644 --- a/MindboxLogger.podspec +++ b/MindboxLogger.podspec @@ -7,7 +7,7 @@ Pod::Spec.new do |spec| spec.license = { :type => "CC BY-NC-ND 4.0", :file => "LICENSE" } spec.author = { "Mindbox" => "ios-sdk@mindbox.ru" } spec.platform = :ios, "10.0" - spec.source = { :git => "https://github.com/mindbox-cloud/ios-sdk.git", :tag => spec.version } + spec.source = { :git => "https://github.com/mindbox-cloud/ios-sdk.git", :tag => "#{spec.version}-logger" } spec.source_files = "MindboxLogger/**/*.{swift}", "SDKVersionProvider/**/*.{swift}" spec.exclude_files = "Classes/Exclude" spec.resource_bundles = { From 3a8d43385bb398646e45661eeb8afc4dc7a5b9dc Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 4 Apr 2024 08:04:32 +0000 Subject: [PATCH 08/35] Update dependency fastlane to v2.220.0 --- Gemfile.lock | 81 ++++++++++++++++++++++++++-------------------------- 1 file changed, 40 insertions(+), 41 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 9d5dcb2b..207ca45e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,9 @@ GEM remote: https://rubygems.org/ specs: - CFPropertyList (3.0.6) + CFPropertyList (3.0.7) + base64 + nkf rexml activesupport (6.1.7.6) concurrent-ruby (~> 1.0, >= 1.0.2) @@ -9,30 +11,31 @@ GEM minitest (>= 5.1) tzinfo (~> 2.0) zeitwerk (~> 2.3) - addressable (2.8.5) + addressable (2.8.6) public_suffix (>= 2.0.2, < 6.0) algoliasearch (1.27.5) httpclient (~> 2.8, >= 2.8.3) json (>= 1.5.1) - artifactory (3.0.15) + artifactory (3.0.17) atomos (0.1.3) - aws-eventstream (1.2.0) - aws-partitions (1.825.0) - aws-sdk-core (3.182.0) - aws-eventstream (~> 1, >= 1.0.2) + aws-eventstream (1.3.0) + aws-partitions (1.907.0) + aws-sdk-core (3.191.6) + aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.651.0) - aws-sigv4 (~> 1.5) + aws-sigv4 (~> 1.8) jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.71.0) - aws-sdk-core (~> 3, >= 3.177.0) + aws-sdk-kms (1.78.0) + aws-sdk-core (~> 3, >= 3.191.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.134.0) - aws-sdk-core (~> 3, >= 3.181.0) + aws-sdk-s3 (1.146.1) + aws-sdk-core (~> 3, >= 3.191.0) aws-sdk-kms (~> 1) - aws-sigv4 (~> 1.6) - aws-sigv4 (1.6.0) + aws-sigv4 (~> 1.8) + aws-sigv4 (1.8.0) aws-eventstream (~> 1, >= 1.0.2) babosa (1.0.4) + base64 (0.2.0) claide (1.1.0) cocoapods (1.13.0) addressable (~> 2.8) @@ -79,14 +82,13 @@ GEM declarative (0.0.20) digest-crc (0.6.5) rake (>= 12.0.0, < 14.0.0) - domain_name (0.5.20190701) - unf (>= 0.0.5, < 1.0.0) + domain_name (0.6.20240107) dotenv (2.8.1) emoji_regex (3.2.3) escape (0.0.4) ethon (0.16.0) ffi (>= 1.15.0) - excon (0.103.0) + excon (0.110.0) faraday (1.10.3) faraday-em_http (~> 1.0) faraday-em_synchrony (~> 1.0) @@ -115,7 +117,7 @@ GEM faraday-retry (1.0.3) faraday_middleware (1.2.0) faraday (~> 1.0) - fastimage (2.2.7) + fastimage (2.3.1) fastlane (2.214.0) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.8, < 3.0.0) @@ -159,9 +161,9 @@ GEM fourflusher (2.3.1) fuzzy_match (2.0.4) gh_inspector (1.1.3) - google-apis-androidpublisher_v3 (0.49.0) + google-apis-androidpublisher_v3 (0.54.0) google-apis-core (>= 0.11.0, < 2.a) - google-apis-core (0.11.1) + google-apis-core (0.11.3) addressable (~> 2.5, >= 2.5.1) googleauth (>= 0.16.2, < 2.a) httpclient (>= 2.8.1, < 3.a) @@ -169,28 +171,27 @@ GEM representable (~> 3.0) retriable (>= 2.0, < 4.a) rexml - webrick google-apis-iamcredentials_v1 (0.17.0) google-apis-core (>= 0.11.0, < 2.a) google-apis-playcustomapp_v1 (0.13.0) google-apis-core (>= 0.11.0, < 2.a) - google-apis-storage_v1 (0.19.0) - google-apis-core (>= 0.9.0, < 2.a) - google-cloud-core (1.6.0) - google-cloud-env (~> 1.0) + google-apis-storage_v1 (0.31.0) + google-apis-core (>= 0.11.0, < 2.a) + google-cloud-core (1.7.0) + google-cloud-env (>= 1.0, < 3.a) google-cloud-errors (~> 1.0) google-cloud-env (1.6.0) faraday (>= 0.17.3, < 3.0) - google-cloud-errors (1.3.1) - google-cloud-storage (1.44.0) + google-cloud-errors (1.4.0) + google-cloud-storage (1.47.0) addressable (~> 2.8) digest-crc (~> 0.4) google-apis-iamcredentials_v1 (~> 0.1) - google-apis-storage_v1 (~> 0.19.0) + google-apis-storage_v1 (~> 0.31.0) google-cloud-core (~> 1.6) googleauth (>= 0.16.2, < 2.a) mini_mime (~> 1.0) - googleauth (1.8.0) + googleauth (1.8.1) faraday (>= 0.17.3, < 3.a) jwt (>= 1.4, < 3.0) multi_json (~> 1.11) @@ -203,23 +204,25 @@ GEM i18n (1.14.1) concurrent-ruby (~> 1.0) jmespath (1.6.2) - json (2.6.3) - jwt (2.7.1) + json (2.7.2) + jwt (2.8.1) + base64 mini_magick (4.12.0) mini_mime (1.1.5) minitest (5.20.0) molinillo (0.8.0) multi_json (1.15.0) - multipart-post (2.3.0) + multipart-post (2.4.0) nanaimo (0.3.0) nap (1.1.0) naturally (2.2.1) netrc (0.11.0) + nkf (0.2.0) optparse (0.1.1) os (1.1.4) - plist (3.7.0) + plist (3.7.1) public_suffix (4.0.7) - rake (13.0.6) + rake (13.2.0) representable (3.2.0) declarative (< 0.1.0) trailblazer-option (>= 0.1.1, < 0.2.0) @@ -231,7 +234,7 @@ GEM ruby2_keywords (0.0.5) rubyzip (2.3.2) security (0.1.3) - signet (0.18.0) + signet (0.19.0) addressable (~> 2.8) faraday (>= 0.17.5, < 3.a) jwt (>= 1.5, < 3.0) @@ -244,7 +247,7 @@ GEM unicode-display_width (~> 1.1, >= 1.1.1) trailblazer-option (0.1.2) tty-cursor (0.7.1) - tty-screen (0.8.1) + tty-screen (0.8.2) tty-spinner (0.9.3) tty-cursor (~> 0.7) typhoeus (1.4.1) @@ -252,13 +255,9 @@ GEM tzinfo (2.0.6) concurrent-ruby (~> 1.0) uber (0.1.0) - unf (0.1.4) - unf_ext - unf_ext (0.0.8.2) unicode-display_width (1.8.0) - webrick (1.8.1) word_wrap (1.0.0) - xcodeproj (1.23.0) + xcodeproj (1.24.0) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) claide (>= 1.0.2, < 2.0) From a00d33878b3a6079a40211c38d656223fc73a8a4 Mon Sep 17 00:00:00 2001 From: Akylbek Utekeshev Date: Thu, 4 Apr 2024 13:03:59 +0500 Subject: [PATCH 09/35] MBX-0000 Run workflow by pushing branch w existing check --- .github/workflows/prepare_release_branch.yml | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/.github/workflows/prepare_release_branch.yml b/.github/workflows/prepare_release_branch.yml index 9447361b..364862c8 100644 --- a/.github/workflows/prepare_release_branch.yml +++ b/.github/workflows/prepare_release_branch.yml @@ -2,8 +2,10 @@ name: Preparation for release on: push: - branches: - - 'release/*' + branches: + - 'feature/MBX-0000-AutoCreateReleaseBranch' + tags: + - '*' jobs: preparation: @@ -13,7 +15,15 @@ jobs: uses: actions/checkout@v4 - name: Extract version from branch name - run: echo "VERSION=$(echo ${GITHUB_REF#refs/heads/release/})" >> $GITHUB_ENV + run: echo "VERSION=${GITHUB_REF#refs/heads/release/}" >> $GITHUB_ENV + + - name: Check for existing branch + run: | + if git ls-remote --heads origin release/${{ env.VERSION }} | grep release/${{ env.VERSION }}; then + echo "Branch release/${{ env.VERSION }} already exists." + exit 1 + fi + shell: bash - name: Check if sdkVersion matches VERSION run: | From 2543bd928fd32e0a56e091460b46949ad25def0c Mon Sep 17 00:00:00 2001 From: Akylbek Utekeshev Date: Thu, 4 Apr 2024 14:40:02 +0500 Subject: [PATCH 10/35] MBX-0000 comment code for tests --- .github/workflows/prepare_release_branch.yml | 112 +++++++++---------- 1 file changed, 56 insertions(+), 56 deletions(-) diff --git a/.github/workflows/prepare_release_branch.yml b/.github/workflows/prepare_release_branch.yml index 364862c8..5e1d328c 100644 --- a/.github/workflows/prepare_release_branch.yml +++ b/.github/workflows/prepare_release_branch.yml @@ -25,6 +25,9 @@ jobs: fi shell: bash + - name: Bump version + run: ./git-release-branch-create.sh "${{ env.VERSION }}" + - name: Check if sdkVersion matches VERSION run: | SDK_VERSION=$(sed -n 's/^.*sdkVersion = "\(.*\)"/\1/p' SDKVersionProvider/SDKVersionProvider.swift) @@ -34,60 +37,57 @@ jobs: fi shell: bash - - name: Bump version - run: ./git-release-branch-create.sh "${{ env.VERSION }}" - - - name: Create Pull Request - id: create-pr - uses: peter-evans/create-pull-request@v4 - with: - commit-message: Update release version to ${{ env.VERSION }} - title: "Release/${{ env.VERSION }}" - body: | - Updates the release version to ${{ env.VERSION }}. - branch: "release/${{ env.VERSION }}" - base: "master" + # - name: Create Pull Request + # id: create-pr + # uses: peter-evans/create-pull-request@v4 + # with: + # commit-message: Update release version to ${{ env.VERSION }} + # title: "Release/${{ env.VERSION }}" + # body: | + # Updates the release version to ${{ env.VERSION }}. + # branch: "release/${{ env.VERSION }}" + # base: "master" - - name: Post to a Slack channel - id: slack - uses: slackapi/slack-github-action@v1.25.0 - with: - channel-id: 'C06RXV161RA' - payload: | - { - "text": "iOS Release Branch Notification", - "blocks": [ - { - "type": "header", - "text": { - "type": "plain_text", - "text": "🚀 iOS Release Branch Created" - } - }, - { - "type": "section", - "text": { - "type": "mrkdwn", - "text": "*Branch Name:* *`release/${{ env.VERSION }}`*\n*Status:* Success :white_check_mark:" - } - }, - { - "type": "divider" - }, - { - "type": "actions", - "elements": [ - { - "type": "button", - "text": { - "type": "plain_text", - "text": "Pull Request" - }, - "url": "${{ steps.create-pr.outputs.pull-request-url }}" - } - ] - } - ] - } - env: - SLACK_BOT_TOKEN: ${{ secrets.SLACK_MOBILE_NOTIFIER_TOKEN }} \ No newline at end of file + # - name: Post to a Slack channel + # id: slack + # uses: slackapi/slack-github-action@v1.25.0 + # with: + # channel-id: 'C06RXV161RA' + # payload: | + # { + # "text": "iOS Release Branch Notification", + # "blocks": [ + # { + # "type": "header", + # "text": { + # "type": "plain_text", + # "text": "🚀 iOS Release Branch Created" + # } + # }, + # { + # "type": "section", + # "text": { + # "type": "mrkdwn", + # "text": "*Branch Name:* *`release/${{ env.VERSION }}`*\n*Status:* Success :white_check_mark:" + # } + # }, + # { + # "type": "divider" + # }, + # { + # "type": "actions", + # "elements": [ + # { + # "type": "button", + # "text": { + # "type": "plain_text", + # "text": "Pull Request" + # }, + # "url": "${{ steps.create-pr.outputs.pull-request-url }}" + # } + # ] + # } + # ] + # } + # env: + # SLACK_BOT_TOKEN: ${{ secrets.SLACK_MOBILE_NOTIFIER_TOKEN }} \ No newline at end of file From 1d25282285687285e76cc8bd9f7dbf34a5609fd5 Mon Sep 17 00:00:00 2001 From: Akylbek Utekeshev Date: Thu, 4 Apr 2024 15:09:29 +0500 Subject: [PATCH 11/35] MBX-0000 uncomment code --- .github/workflows/prepare_release_branch.yml | 108 +++++++++---------- 1 file changed, 53 insertions(+), 55 deletions(-) diff --git a/.github/workflows/prepare_release_branch.yml b/.github/workflows/prepare_release_branch.yml index 5e1d328c..6eead48b 100644 --- a/.github/workflows/prepare_release_branch.yml +++ b/.github/workflows/prepare_release_branch.yml @@ -2,8 +2,6 @@ name: Preparation for release on: push: - branches: - - 'feature/MBX-0000-AutoCreateReleaseBranch' tags: - '*' @@ -37,57 +35,57 @@ jobs: fi shell: bash - # - name: Create Pull Request - # id: create-pr - # uses: peter-evans/create-pull-request@v4 - # with: - # commit-message: Update release version to ${{ env.VERSION }} - # title: "Release/${{ env.VERSION }}" - # body: | - # Updates the release version to ${{ env.VERSION }}. - # branch: "release/${{ env.VERSION }}" - # base: "master" + - name: Create Pull Request + id: create-pr + uses: peter-evans/create-pull-request@v4 + with: + commit-message: Update release version to ${{ env.VERSION }} + title: "Release/${{ env.VERSION }}" + body: | + Updates the release version to ${{ env.VERSION }}. + branch: "release/${{ env.VERSION }}" + base: "master" - # - name: Post to a Slack channel - # id: slack - # uses: slackapi/slack-github-action@v1.25.0 - # with: - # channel-id: 'C06RXV161RA' - # payload: | - # { - # "text": "iOS Release Branch Notification", - # "blocks": [ - # { - # "type": "header", - # "text": { - # "type": "plain_text", - # "text": "🚀 iOS Release Branch Created" - # } - # }, - # { - # "type": "section", - # "text": { - # "type": "mrkdwn", - # "text": "*Branch Name:* *`release/${{ env.VERSION }}`*\n*Status:* Success :white_check_mark:" - # } - # }, - # { - # "type": "divider" - # }, - # { - # "type": "actions", - # "elements": [ - # { - # "type": "button", - # "text": { - # "type": "plain_text", - # "text": "Pull Request" - # }, - # "url": "${{ steps.create-pr.outputs.pull-request-url }}" - # } - # ] - # } - # ] - # } - # env: - # SLACK_BOT_TOKEN: ${{ secrets.SLACK_MOBILE_NOTIFIER_TOKEN }} \ No newline at end of file + - name: Post to a Slack channel + id: slack + uses: slackapi/slack-github-action@v1.25.0 + with: + channel-id: 'C06RXV161RA' + payload: | + { + "text": "iOS Release Branch Notification", + "blocks": [ + { + "type": "header", + "text": { + "type": "plain_text", + "text": "🚀 iOS Release Branch Created" + } + }, + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "*Branch Name:* *`release/${{ env.VERSION }}`*\n*Status:* Success :white_check_mark:" + } + }, + { + "type": "divider" + }, + { + "type": "actions", + "elements": [ + { + "type": "button", + "text": { + "type": "plain_text", + "text": "Pull Request" + }, + "url": "${{ steps.create-pr.outputs.pull-request-url }}" + } + ] + } + ] + } + env: + SLACK_BOT_TOKEN: ${{ secrets.SLACK_MOBILE_NOTIFIER_TOKEN }} \ No newline at end of file From 7645d0ef06c697b108c2e9505ef3b035737a5e0a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 4 Apr 2024 10:18:52 +0000 Subject: [PATCH 12/35] Update peter-evans/create-pull-request action to v6 --- .github/workflows/prepare_release_branch.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/prepare_release_branch.yml b/.github/workflows/prepare_release_branch.yml index 6eead48b..42ab0345 100644 --- a/.github/workflows/prepare_release_branch.yml +++ b/.github/workflows/prepare_release_branch.yml @@ -37,7 +37,7 @@ jobs: - name: Create Pull Request id: create-pr - uses: peter-evans/create-pull-request@v4 + uses: peter-evans/create-pull-request@v6 with: commit-message: Update release version to ${{ env.VERSION }} title: "Release/${{ env.VERSION }}" From 4fcc17077432d3496a6a64552cac12b1468fe309 Mon Sep 17 00:00:00 2001 From: Dmitry Erofeev <56806136+kniksees@users.noreply.github.com> Date: Thu, 4 Apr 2024 15:34:51 +0300 Subject: [PATCH 13/35] MBX-3248: Example (#323) * feature/MBX-3248-ExampleApp: add ExampleApp * feature/MBX-3248-ExampleApp: change MainViewModel init * feature/MBX-3248-ExampleApp: code formatting * feature/MBX-3248-ExampleApp: add rich-push * feature/MBX-3248-ExampleApp: add MindboxNotificationContentExtension * feature/MBX-3248-ExampleApp: add InAppMessagesDelegate * feature/MBX-3248-ExampleApp: UI changing * feature/MBX-3248-ExampleApp: add ChooseInappMessageDelegate enum * feature/MBX-3248-ExampleApp: add comments * feature/MBX-3248-ExampleApp: code formatting * feature/MBX-3248-ExampleApp: Create README.md * feature/MBX-3248-ExampleApp: Update README.md * feature/MBX-3248-ExampleApp: Update README.md * feature/MBX-3248-ExampleApp: Update README.md * feature/MBX-3248-ExampleApp: Update README.md * feature/MBX-3248-ExampleApp: Update README.md * feature/MBX-3248-ExampleApp: Update README.md * feature/MBX-3248-ExampleApp: remove folder * feature/MBX-3248-ExampleApp: add ChooseInappMessageDelegate * feature/MBX-3248-ExampleApp: update Podfile * feature/MBX-3248-ExampleApp: update viewModel * feature/MBX-3248-ExampleApp: change icon * feature/MBX-3248-ExampleApp: update LounchScreen * feature/MBX-3248-ExampleApp: code alignment * feature/MBX-3248-ExampleApp: update launchscreen * feature/MBX-3248-ExampleApp: update launchscreen * feature/MBX-3248-ExampleApp: update launchscreen * feature/MBX-3248-ExampleApp: update SceneDelegate * feature/MBX-3248-ExampleApp: move ChooseInAppMessagesDelegate from AppDelegate to ViewModel * feature/MBX-3248-ExampleApp: Update README.md * feature/MBX-3248-ExampleApp: remove extra launch screen * feature/MBX-3248-ExampleApp: Update README.md * feature/MBX-3248-ExampleApp: Update README.md * feature/MBX-3248-ExampleApp: Update README.md * feature/MBX-3248-ExampleApp: Update README.md --- Example/Example.xcodeproj/project.pbxproj | 837 ++++++++++++++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/xcschemes/Example.xcscheme | 78 ++ .../contents.xcworkspacedata | 10 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + Example/Example/AppDelegate.swift | 72 ++ .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/AppIcon1024x1024.png | Bin 0 -> 47711 bytes .../AppIcon.appiconset/Contents.json | 14 + Example/Example/Assets.xcassets/Contents.json | 6 + .../MBBackground.colorset/Contents.json | 38 + .../MBGreen.colorset/Contents.json | 38 + .../MBText.colorset/Contents.json | 38 + .../MBView.colorset/Contents.json | 38 + .../m-green_Main_large.imageset/Contents.json | 23 + .../m-green_Main_large 1.png | Bin 0 -> 48197 bytes .../m-green_Main_large 2.png | Bin 0 -> 48197 bytes .../m-green_Main_large.png | Bin 0 -> 48197 bytes Example/Example/Example.entitlements | 12 + Example/Example/Info.plist | 29 + Example/Example/LaunchScreen.storyboard | 47 + Example/Example/PrivacyInfo.xcprivacy | 48 + Example/Example/SceneDelegate.swift | 24 + .../View/CustomVIews/ButtonsView.swift | 54 ++ .../View/CustomVIews/SDKDataView.swift | 73 ++ Example/Example/View/MainView.swift | 38 + .../ChooseInAppMessagesDelegate.swift | 45 + Example/Example/ViewModel/MainViewModel.swift | 72 ++ .../Info.plist | 22 + ...xNotificationContentExtension.entitlements | 10 + .../NotificationViewController.swift | 20 + .../Info.plist | 13 + ...xNotificationServiceExtension.entitlements | 10 + .../NotificationService.swift | 24 + Example/Podfile | 13 + Example/Podfile.lock | 25 + Example/README.md | 65 ++ 38 files changed, 1870 insertions(+) create mode 100644 Example/Example.xcodeproj/project.pbxproj create mode 100644 Example/Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 Example/Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 Example/Example.xcodeproj/xcshareddata/xcschemes/Example.xcscheme create mode 100644 Example/Example.xcworkspace/contents.xcworkspacedata create mode 100644 Example/Example.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 Example/Example/AppDelegate.swift create mode 100644 Example/Example/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 Example/Example/Assets.xcassets/AppIcon.appiconset/AppIcon1024x1024.png create mode 100644 Example/Example/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 Example/Example/Assets.xcassets/Contents.json create mode 100644 Example/Example/Assets.xcassets/MBBackground.colorset/Contents.json create mode 100644 Example/Example/Assets.xcassets/MBGreen.colorset/Contents.json create mode 100644 Example/Example/Assets.xcassets/MBText.colorset/Contents.json create mode 100644 Example/Example/Assets.xcassets/MBView.colorset/Contents.json create mode 100644 Example/Example/Assets.xcassets/m-green_Main_large.imageset/Contents.json create mode 100644 Example/Example/Assets.xcassets/m-green_Main_large.imageset/m-green_Main_large 1.png create mode 100644 Example/Example/Assets.xcassets/m-green_Main_large.imageset/m-green_Main_large 2.png create mode 100644 Example/Example/Assets.xcassets/m-green_Main_large.imageset/m-green_Main_large.png create mode 100644 Example/Example/Example.entitlements create mode 100644 Example/Example/Info.plist create mode 100644 Example/Example/LaunchScreen.storyboard create mode 100644 Example/Example/PrivacyInfo.xcprivacy create mode 100644 Example/Example/SceneDelegate.swift create mode 100644 Example/Example/View/CustomVIews/ButtonsView.swift create mode 100644 Example/Example/View/CustomVIews/SDKDataView.swift create mode 100644 Example/Example/View/MainView.swift create mode 100644 Example/Example/ViewModel/ChooseInAppMessagesDelegate.swift create mode 100644 Example/Example/ViewModel/MainViewModel.swift create mode 100644 Example/MindboxNotificationContentExtension/Info.plist create mode 100644 Example/MindboxNotificationContentExtension/MindboxNotificationContentExtension.entitlements create mode 100644 Example/MindboxNotificationContentExtension/NotificationViewController.swift create mode 100644 Example/MindboxNotificationServiceExtension/Info.plist create mode 100644 Example/MindboxNotificationServiceExtension/MindboxNotificationServiceExtension.entitlements create mode 100644 Example/MindboxNotificationServiceExtension/NotificationService.swift create mode 100644 Example/Podfile create mode 100644 Example/Podfile.lock create mode 100644 Example/README.md diff --git a/Example/Example.xcodeproj/project.pbxproj b/Example/Example.xcodeproj/project.pbxproj new file mode 100644 index 00000000..4aedae17 --- /dev/null +++ b/Example/Example.xcodeproj/project.pbxproj @@ -0,0 +1,837 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + 0A0DE3482BB8455A00812E73 /* NotificationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A0DE3472BB8455A00812E73 /* NotificationService.swift */; }; + 0A0DE34C2BB8455A00812E73 /* MindboxNotificationServiceExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 0A0DE3452BB8455A00812E73 /* MindboxNotificationServiceExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 0A4B681A2BBC82B500639BC5 /* ChooseInAppMessagesDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A4B68192BBC82B500639BC5 /* ChooseInAppMessagesDelegate.swift */; }; + 0A4B682A2BBD7D5100639BC5 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0A4B68292BBD7D5100639BC5 /* LaunchScreen.storyboard */; }; + 0AD271D02BB9D81E00750279 /* UserNotifications.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0AD271CF2BB9D81D00750279 /* UserNotifications.framework */; }; + 0AD271D22BB9D81E00750279 /* UserNotificationsUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0AD271D12BB9D81E00750279 /* UserNotificationsUI.framework */; }; + 0AD271D52BB9D81E00750279 /* NotificationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AD271D42BB9D81E00750279 /* NotificationViewController.swift */; }; + 0AD271DC2BB9D81E00750279 /* MindboxNotificationContentExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 0AD271CE2BB9D81D00750279 /* MindboxNotificationContentExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; + 0AD271E42BBAAA8800750279 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 0AD271E32BBAAA8800750279 /* PrivacyInfo.xcprivacy */; }; + 0AEDBC7A2BB6F8F200EE8722 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AEDBC792BB6F8F200EE8722 /* AppDelegate.swift */; }; + 0AEDBC7C2BB6F8F200EE8722 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AEDBC7B2BB6F8F200EE8722 /* SceneDelegate.swift */; }; + 0AEDBC832BB6F8F400EE8722 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0AEDBC822BB6F8F400EE8722 /* Assets.xcassets */; }; + 0AEDBC922BB6FA4800EE8722 /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AEDBC912BB6FA4800EE8722 /* MainView.swift */; }; + 0AEDBC962BB6FE4900EE8722 /* MainViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AEDBC952BB6FE4900EE8722 /* MainViewModel.swift */; }; + 0AEDBC992BB7058B00EE8722 /* ButtonsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AEDBC982BB7058B00EE8722 /* ButtonsView.swift */; }; + 0AEDBC9B2BB70D6E00EE8722 /* SDKDataView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AEDBC9A2BB70D6E00EE8722 /* SDKDataView.swift */; }; + A9A0D2B56DA740D1AC14A76E /* Pods_MindboxNotificationContentExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B48D4AD5551A8E0F2B521ED8 /* Pods_MindboxNotificationContentExtension.framework */; }; + EC719A83A7838F46B132825D /* Pods_Example.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D80D2646AFEBEE6F556A82E3 /* Pods_Example.framework */; }; + ECBF861DEC24D002BCCA5C54 /* Pods_MindboxNotificationServiceExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6D0E4C1407865DB9AA01F215 /* Pods_MindboxNotificationServiceExtension.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 0A0DE34A2BB8455A00812E73 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 0AEDBC6E2BB6F8F200EE8722 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 0A0DE3442BB8455A00812E73; + remoteInfo = MindboxNotificationServiceExtension; + }; + 0AD271DA2BB9D81E00750279 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 0AEDBC6E2BB6F8F200EE8722 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 0AD271CD2BB9D81D00750279; + remoteInfo = MindboxNotificationContentExtension; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 0A0DE34D2BB8455A00812E73 /* Embed Foundation Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 13; + files = ( + 0A0DE34C2BB8455A00812E73 /* MindboxNotificationServiceExtension.appex in Embed Foundation Extensions */, + 0AD271DC2BB9D81E00750279 /* MindboxNotificationContentExtension.appex in Embed Foundation Extensions */, + ); + name = "Embed Foundation Extensions"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 0A0DE3452BB8455A00812E73 /* MindboxNotificationServiceExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = MindboxNotificationServiceExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + 0A0DE3472BB8455A00812E73 /* NotificationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationService.swift; sourceTree = ""; }; + 0A0DE3492BB8455A00812E73 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 0A0DE3512BB8457100812E73 /* MindboxNotificationServiceExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = MindboxNotificationServiceExtension.entitlements; sourceTree = ""; }; + 0A4B68192BBC82B500639BC5 /* ChooseInAppMessagesDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChooseInAppMessagesDelegate.swift; sourceTree = ""; }; + 0A4B68292BBD7D5100639BC5 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = ""; }; + 0AD271CE2BB9D81D00750279 /* MindboxNotificationContentExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = MindboxNotificationContentExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + 0AD271CF2BB9D81D00750279 /* UserNotifications.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UserNotifications.framework; path = System/Library/Frameworks/UserNotifications.framework; sourceTree = SDKROOT; }; + 0AD271D12BB9D81E00750279 /* UserNotificationsUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UserNotificationsUI.framework; path = System/Library/Frameworks/UserNotificationsUI.framework; sourceTree = SDKROOT; }; + 0AD271D42BB9D81E00750279 /* NotificationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationViewController.swift; sourceTree = ""; }; + 0AD271D92BB9D81E00750279 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 0AD271E02BB9D84800750279 /* MindboxNotificationContentExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = MindboxNotificationContentExtension.entitlements; sourceTree = ""; }; + 0AD271E32BBAAA8800750279 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; + 0AEDBC762BB6F8F200EE8722 /* Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Example.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 0AEDBC792BB6F8F200EE8722 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 0AEDBC7B2BB6F8F200EE8722 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; + 0AEDBC822BB6F8F400EE8722 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 0AEDBC872BB6F8F400EE8722 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 0AEDBC912BB6FA4800EE8722 /* MainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = ""; }; + 0AEDBC952BB6FE4900EE8722 /* MainViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainViewModel.swift; sourceTree = ""; }; + 0AEDBC982BB7058B00EE8722 /* ButtonsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonsView.swift; sourceTree = ""; }; + 0AEDBC9A2BB70D6E00EE8722 /* SDKDataView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SDKDataView.swift; sourceTree = ""; }; + 0AEDBC9C2BB7177A00EE8722 /* Example.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Example.entitlements; sourceTree = ""; }; + 240E950B149EDBB0CAC016A0 /* Pods-Example.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Example.debug.xcconfig"; path = "Target Support Files/Pods-Example/Pods-Example.debug.xcconfig"; sourceTree = ""; }; + 296B8C4BB0F741F658A9E34E /* Pods-MindboxNotificationContentExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MindboxNotificationContentExtension.debug.xcconfig"; path = "Target Support Files/Pods-MindboxNotificationContentExtension/Pods-MindboxNotificationContentExtension.debug.xcconfig"; sourceTree = ""; }; + 6D0E4C1407865DB9AA01F215 /* Pods_MindboxNotificationServiceExtension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_MindboxNotificationServiceExtension.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 8800E015E929FCDAB94398CB /* Pods-Example.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Example.release.xcconfig"; path = "Target Support Files/Pods-Example/Pods-Example.release.xcconfig"; sourceTree = ""; }; + 8FC128008EAD005E4940F3B6 /* Pods-MindboxNotificationServiceExtension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MindboxNotificationServiceExtension.release.xcconfig"; path = "Target Support Files/Pods-MindboxNotificationServiceExtension/Pods-MindboxNotificationServiceExtension.release.xcconfig"; sourceTree = ""; }; + 97056FE3D936C1A512C53307 /* Pods-MindboxNotificationContentExtension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MindboxNotificationContentExtension.release.xcconfig"; path = "Target Support Files/Pods-MindboxNotificationContentExtension/Pods-MindboxNotificationContentExtension.release.xcconfig"; sourceTree = ""; }; + AFAF38BA3FE3FE7CDC56ADB5 /* Pods-MindboxNotificationServiceExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-MindboxNotificationServiceExtension.debug.xcconfig"; path = "Target Support Files/Pods-MindboxNotificationServiceExtension/Pods-MindboxNotificationServiceExtension.debug.xcconfig"; sourceTree = ""; }; + B48D4AD5551A8E0F2B521ED8 /* Pods_MindboxNotificationContentExtension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_MindboxNotificationContentExtension.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D80D2646AFEBEE6F556A82E3 /* Pods_Example.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Example.framework; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 0A0DE3422BB8455A00812E73 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ECBF861DEC24D002BCCA5C54 /* Pods_MindboxNotificationServiceExtension.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 0AD271CB2BB9D81D00750279 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 0AD271D22BB9D81E00750279 /* UserNotificationsUI.framework in Frameworks */, + 0AD271D02BB9D81E00750279 /* UserNotifications.framework in Frameworks */, + A9A0D2B56DA740D1AC14A76E /* Pods_MindboxNotificationContentExtension.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 0AEDBC732BB6F8F200EE8722 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + EC719A83A7838F46B132825D /* Pods_Example.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 0A0DE3462BB8455A00812E73 /* MindboxNotificationServiceExtension */ = { + isa = PBXGroup; + children = ( + 0A0DE3512BB8457100812E73 /* MindboxNotificationServiceExtension.entitlements */, + 0A0DE3472BB8455A00812E73 /* NotificationService.swift */, + 0A0DE3492BB8455A00812E73 /* Info.plist */, + ); + path = MindboxNotificationServiceExtension; + sourceTree = ""; + }; + 0AD271D32BB9D81E00750279 /* MindboxNotificationContentExtension */ = { + isa = PBXGroup; + children = ( + 0AD271E02BB9D84800750279 /* MindboxNotificationContentExtension.entitlements */, + 0AD271D42BB9D81E00750279 /* NotificationViewController.swift */, + 0AD271D92BB9D81E00750279 /* Info.plist */, + ); + path = MindboxNotificationContentExtension; + sourceTree = ""; + }; + 0AEDBC6D2BB6F8F200EE8722 = { + isa = PBXGroup; + children = ( + 0AEDBC782BB6F8F200EE8722 /* Example */, + 0A0DE3462BB8455A00812E73 /* MindboxNotificationServiceExtension */, + 0AD271D32BB9D81E00750279 /* MindboxNotificationContentExtension */, + 0AEDBC772BB6F8F200EE8722 /* Products */, + D07217A390B79F6F292D4AFC /* Pods */, + 52C896E46FFA4E326FC946A8 /* Frameworks */, + ); + sourceTree = ""; + }; + 0AEDBC772BB6F8F200EE8722 /* Products */ = { + isa = PBXGroup; + children = ( + 0AEDBC762BB6F8F200EE8722 /* Example.app */, + 0A0DE3452BB8455A00812E73 /* MindboxNotificationServiceExtension.appex */, + 0AD271CE2BB9D81D00750279 /* MindboxNotificationContentExtension.appex */, + ); + name = Products; + sourceTree = ""; + }; + 0AEDBC782BB6F8F200EE8722 /* Example */ = { + isa = PBXGroup; + children = ( + 0AEDBC9C2BB7177A00EE8722 /* Example.entitlements */, + 0AEDBC792BB6F8F200EE8722 /* AppDelegate.swift */, + 0AD271E32BBAAA8800750279 /* PrivacyInfo.xcprivacy */, + 0AEDBC7B2BB6F8F200EE8722 /* SceneDelegate.swift */, + 0AEDBC822BB6F8F400EE8722 /* Assets.xcassets */, + 0A4B68292BBD7D5100639BC5 /* LaunchScreen.storyboard */, + 0AEDBC872BB6F8F400EE8722 /* Info.plist */, + 0AEDBC9E2BB71FD700EE8722 /* ViewModel */, + 0AEDBC9D2BB71FC600EE8722 /* View */, + ); + path = Example; + sourceTree = ""; + }; + 0AEDBC972BB7050F00EE8722 /* CustomVIews */ = { + isa = PBXGroup; + children = ( + 0AEDBC982BB7058B00EE8722 /* ButtonsView.swift */, + 0AEDBC9A2BB70D6E00EE8722 /* SDKDataView.swift */, + ); + path = CustomVIews; + sourceTree = ""; + }; + 0AEDBC9D2BB71FC600EE8722 /* View */ = { + isa = PBXGroup; + children = ( + 0AEDBC972BB7050F00EE8722 /* CustomVIews */, + 0AEDBC912BB6FA4800EE8722 /* MainView.swift */, + ); + path = View; + sourceTree = ""; + }; + 0AEDBC9E2BB71FD700EE8722 /* ViewModel */ = { + isa = PBXGroup; + children = ( + 0AEDBC952BB6FE4900EE8722 /* MainViewModel.swift */, + 0A4B68192BBC82B500639BC5 /* ChooseInAppMessagesDelegate.swift */, + ); + path = ViewModel; + sourceTree = ""; + }; + 52C896E46FFA4E326FC946A8 /* Frameworks */ = { + isa = PBXGroup; + children = ( + D80D2646AFEBEE6F556A82E3 /* Pods_Example.framework */, + 6D0E4C1407865DB9AA01F215 /* Pods_MindboxNotificationServiceExtension.framework */, + 0AD271CF2BB9D81D00750279 /* UserNotifications.framework */, + 0AD271D12BB9D81E00750279 /* UserNotificationsUI.framework */, + B48D4AD5551A8E0F2B521ED8 /* Pods_MindboxNotificationContentExtension.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + D07217A390B79F6F292D4AFC /* Pods */ = { + isa = PBXGroup; + children = ( + 240E950B149EDBB0CAC016A0 /* Pods-Example.debug.xcconfig */, + 8800E015E929FCDAB94398CB /* Pods-Example.release.xcconfig */, + AFAF38BA3FE3FE7CDC56ADB5 /* Pods-MindboxNotificationServiceExtension.debug.xcconfig */, + 8FC128008EAD005E4940F3B6 /* Pods-MindboxNotificationServiceExtension.release.xcconfig */, + 296B8C4BB0F741F658A9E34E /* Pods-MindboxNotificationContentExtension.debug.xcconfig */, + 97056FE3D936C1A512C53307 /* Pods-MindboxNotificationContentExtension.release.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 0A0DE3442BB8455A00812E73 /* MindboxNotificationServiceExtension */ = { + isa = PBXNativeTarget; + buildConfigurationList = 0A0DE3502BB8455A00812E73 /* Build configuration list for PBXNativeTarget "MindboxNotificationServiceExtension" */; + buildPhases = ( + 4A515FC593E4F8BE59A54487 /* [CP] Check Pods Manifest.lock */, + 0A0DE3412BB8455A00812E73 /* Sources */, + 0A0DE3422BB8455A00812E73 /* Frameworks */, + 0A0DE3432BB8455A00812E73 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = MindboxNotificationServiceExtension; + productName = MindboxNotificationServiceExtension; + productReference = 0A0DE3452BB8455A00812E73 /* MindboxNotificationServiceExtension.appex */; + productType = "com.apple.product-type.app-extension"; + }; + 0AD271CD2BB9D81D00750279 /* MindboxNotificationContentExtension */ = { + isa = PBXNativeTarget; + buildConfigurationList = 0AD271DF2BB9D81E00750279 /* Build configuration list for PBXNativeTarget "MindboxNotificationContentExtension" */; + buildPhases = ( + B467E7F45CD3917D961596F6 /* [CP] Check Pods Manifest.lock */, + 0AD271CA2BB9D81D00750279 /* Sources */, + 0AD271CB2BB9D81D00750279 /* Frameworks */, + 0AD271CC2BB9D81D00750279 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = MindboxNotificationContentExtension; + productName = MindboxNotificationContentExtension; + productReference = 0AD271CE2BB9D81D00750279 /* MindboxNotificationContentExtension.appex */; + productType = "com.apple.product-type.app-extension"; + }; + 0AEDBC752BB6F8F200EE8722 /* Example */ = { + isa = PBXNativeTarget; + buildConfigurationList = 0AEDBC8A2BB6F8F400EE8722 /* Build configuration list for PBXNativeTarget "Example" */; + buildPhases = ( + BD4E3170B25EA8DE299726F5 /* [CP] Check Pods Manifest.lock */, + 0AEDBC722BB6F8F200EE8722 /* Sources */, + 0AEDBC732BB6F8F200EE8722 /* Frameworks */, + 0AEDBC742BB6F8F200EE8722 /* Resources */, + AEC74811D64186EB15BEEA5B /* [CP] Embed Pods Frameworks */, + 0A0DE34D2BB8455A00812E73 /* Embed Foundation Extensions */, + ); + buildRules = ( + ); + dependencies = ( + 0A0DE34B2BB8455A00812E73 /* PBXTargetDependency */, + 0AD271DB2BB9D81E00750279 /* PBXTargetDependency */, + ); + name = Example; + productName = Example; + productReference = 0AEDBC762BB6F8F200EE8722 /* Example.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 0AEDBC6E2BB6F8F200EE8722 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1530; + LastUpgradeCheck = 1530; + TargetAttributes = { + 0A0DE3442BB8455A00812E73 = { + CreatedOnToolsVersion = 15.3; + }; + 0AD271CD2BB9D81D00750279 = { + CreatedOnToolsVersion = 15.3; + }; + 0AEDBC752BB6F8F200EE8722 = { + CreatedOnToolsVersion = 15.3; + }; + }; + }; + buildConfigurationList = 0AEDBC712BB6F8F200EE8722 /* Build configuration list for PBXProject "Example" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 0AEDBC6D2BB6F8F200EE8722; + productRefGroup = 0AEDBC772BB6F8F200EE8722 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 0AEDBC752BB6F8F200EE8722 /* Example */, + 0A0DE3442BB8455A00812E73 /* MindboxNotificationServiceExtension */, + 0AD271CD2BB9D81D00750279 /* MindboxNotificationContentExtension */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 0A0DE3432BB8455A00812E73 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 0AD271CC2BB9D81D00750279 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 0AEDBC742BB6F8F200EE8722 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 0A4B682A2BBD7D5100639BC5 /* LaunchScreen.storyboard in Resources */, + 0AD271E42BBAAA8800750279 /* PrivacyInfo.xcprivacy in Resources */, + 0AEDBC832BB6F8F400EE8722 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 4A515FC593E4F8BE59A54487 /* [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-MindboxNotificationServiceExtension-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; + }; + AEC74811D64186EB15BEEA5B /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Example/Pods-Example-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Example/Pods-Example-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Example/Pods-Example-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + B467E7F45CD3917D961596F6 /* [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-MindboxNotificationContentExtension-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; + }; + BD4E3170B25EA8DE299726F5 /* [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-Example-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 */ + 0A0DE3412BB8455A00812E73 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 0A0DE3482BB8455A00812E73 /* NotificationService.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 0AD271CA2BB9D81D00750279 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 0AD271D52BB9D81E00750279 /* NotificationViewController.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 0AEDBC722BB6F8F200EE8722 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 0AEDBC7A2BB6F8F200EE8722 /* AppDelegate.swift in Sources */, + 0AEDBC992BB7058B00EE8722 /* ButtonsView.swift in Sources */, + 0AEDBC922BB6FA4800EE8722 /* MainView.swift in Sources */, + 0AEDBC962BB6FE4900EE8722 /* MainViewModel.swift in Sources */, + 0AEDBC7C2BB6F8F200EE8722 /* SceneDelegate.swift in Sources */, + 0AEDBC9B2BB70D6E00EE8722 /* SDKDataView.swift in Sources */, + 0A4B681A2BBC82B500639BC5 /* ChooseInAppMessagesDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 0A0DE34B2BB8455A00812E73 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 0A0DE3442BB8455A00812E73 /* MindboxNotificationServiceExtension */; + targetProxy = 0A0DE34A2BB8455A00812E73 /* PBXContainerItemProxy */; + }; + 0AD271DB2BB9D81E00750279 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 0AD271CD2BB9D81D00750279 /* MindboxNotificationContentExtension */; + targetProxy = 0AD271DA2BB9D81E00750279 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 0A0DE34E2BB8455A00812E73 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = AFAF38BA3FE3FE7CDC56ADB5 /* Pods-MindboxNotificationServiceExtension.debug.xcconfig */; + buildSettings = { + CODE_SIGN_ENTITLEMENTS = MindboxNotificationServiceExtension/MindboxNotificationServiceExtension.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = N39VVWZXXP; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = MindboxNotificationServiceExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = MindboxNotificationServiceExtension; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = cloud.Mindbox.ReleaseExampleIos.MindboxNotificationServiceExtension; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 0A0DE34F2BB8455A00812E73 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 8FC128008EAD005E4940F3B6 /* Pods-MindboxNotificationServiceExtension.release.xcconfig */; + buildSettings = { + CODE_SIGN_ENTITLEMENTS = MindboxNotificationServiceExtension/MindboxNotificationServiceExtension.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = N39VVWZXXP; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = MindboxNotificationServiceExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = MindboxNotificationServiceExtension; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = cloud.Mindbox.ReleaseExampleIos.MindboxNotificationServiceExtension; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 0AD271DD2BB9D81E00750279 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 296B8C4BB0F741F658A9E34E /* Pods-MindboxNotificationContentExtension.debug.xcconfig */; + buildSettings = { + CODE_SIGN_ENTITLEMENTS = MindboxNotificationContentExtension/MindboxNotificationContentExtension.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = N39VVWZXXP; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = MindboxNotificationContentExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = MindboxNotificationContentExtension; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = cloud.Mindbox.ReleaseExampleIos.MindboxNotificationContentExtension; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 0AD271DE2BB9D81E00750279 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 97056FE3D936C1A512C53307 /* Pods-MindboxNotificationContentExtension.release.xcconfig */; + buildSettings = { + CODE_SIGN_ENTITLEMENTS = MindboxNotificationContentExtension/MindboxNotificationContentExtension.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = N39VVWZXXP; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = MindboxNotificationContentExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = MindboxNotificationContentExtension; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = cloud.Mindbox.ReleaseExampleIos.MindboxNotificationContentExtension; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 0AEDBC882BB6F8F400EE8722 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = 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_DOCUMENTATION_COMMENTS = YES; + 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + 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 = 17.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 0AEDBC892BB6F8F400EE8722 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = 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_DOCUMENTATION_COMMENTS = YES; + 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + 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 = 17.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 0AEDBC8B2BB6F8F400EE8722 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 240E950B149EDBB0CAC016A0 /* Pods-Example.debug.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = Example/Example.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = N39VVWZXXP; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = Example/Info.plist; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen.storyboard; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = cloud.Mindbox.ReleaseExampleIos; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 0AEDBC8C2BB6F8F400EE8722 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 8800E015E929FCDAB94398CB /* Pods-Example.release.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = Example/Example.entitlements; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = N39VVWZXXP; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = Example/Info.plist; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen.storyboard; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = cloud.Mindbox.ReleaseExampleIos; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 0A0DE3502BB8455A00812E73 /* Build configuration list for PBXNativeTarget "MindboxNotificationServiceExtension" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 0A0DE34E2BB8455A00812E73 /* Debug */, + 0A0DE34F2BB8455A00812E73 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 0AD271DF2BB9D81E00750279 /* Build configuration list for PBXNativeTarget "MindboxNotificationContentExtension" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 0AD271DD2BB9D81E00750279 /* Debug */, + 0AD271DE2BB9D81E00750279 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 0AEDBC712BB6F8F200EE8722 /* Build configuration list for PBXProject "Example" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 0AEDBC882BB6F8F400EE8722 /* Debug */, + 0AEDBC892BB6F8F400EE8722 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 0AEDBC8A2BB6F8F400EE8722 /* Build configuration list for PBXNativeTarget "Example" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 0AEDBC8B2BB6F8F400EE8722 /* Debug */, + 0AEDBC8C2BB6F8F400EE8722 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 0AEDBC6E2BB6F8F200EE8722 /* Project object */; +} diff --git a/Example/Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Example/Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..919434a6 --- /dev/null +++ b/Example/Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Example/Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Example/Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/Example/Example.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/Example/Example.xcodeproj/xcshareddata/xcschemes/Example.xcscheme b/Example/Example.xcodeproj/xcshareddata/xcschemes/Example.xcscheme new file mode 100644 index 00000000..be9443e2 --- /dev/null +++ b/Example/Example.xcodeproj/xcshareddata/xcschemes/Example.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Example/Example.xcworkspace/contents.xcworkspacedata b/Example/Example.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..a37cf193 --- /dev/null +++ b/Example/Example.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/Example/Example.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Example/Example.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/Example/Example.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/Example/Example/AppDelegate.swift b/Example/Example/AppDelegate.swift new file mode 100644 index 00000000..79b2aec6 --- /dev/null +++ b/Example/Example/AppDelegate.swift @@ -0,0 +1,72 @@ +// +// AppDelegate.swift +// Example +// +// Created by Дмитрий Ерофеев on 29.03.2024. +// + +import Mindbox +import Foundation +import UIKit + +@main +class AppDelegate: MindboxAppDelegate { + + //https://developers.mindbox.ru/docs/ios-sdk-initialization + override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + super.application(application, didFinishLaunchingWithOptions: launchOptions) + + do { + let mindboxSdkConfig = try MBConfiguration( + //To run the application on a physical device you need to change the endpoint + //You should also change the application bundle ID in all targets, more details in the readme + //You can still run the application on the simulator to see In-Apps + endpoint: "Mpush-test.ReleaseExample.IosApp", + domain: "api.mindbox.ru", + subscribeCustomerIfCreated: true, + shouldCreateCustomer: true + ) + Mindbox.shared.initialization(configuration: mindboxSdkConfig) + Mindbox.shared.getDeviceUUID { deviceUUID in + print(deviceUUID) + } + } catch { + print(error) + } + //https://developers.mindbox.ru/docs/ios-send-push-notifications-appdelegate + registerForRemoteNotifications() + return true + } + + func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { + return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) + } + + //https://developers.mindbox.ru/docs/ios-send-push-notifications-appdelegate + func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { + completionHandler([.list, .badge, .sound]) + + //https://developers.mindbox.ru/docs/ios-sdk-methods + print("Is mindbox notification: \(Mindbox.shared.isMindboxPush(userInfo: notification.request.content.userInfo))") + print("Notification data: \(String(describing: Mindbox.shared.getMindboxPushData(userInfo: notification.request.content.userInfo)))") + if let uniqueKey = Mindbox.shared.getMindboxPushData(userInfo: notification.request.content.userInfo)?.uniqueKey { + Mindbox.shared.pushClicked(uniqueKey: uniqueKey) + } + } + + //https://developers.mindbox.ru/docs/ios-send-push-notifications-appdelegate + func registerForRemoteNotifications() { + UNUserNotificationCenter.current().delegate = self + DispatchQueue.main.async { + UIApplication.shared.registerForRemoteNotifications() + UNUserNotificationCenter.current().requestAuthorization(options: [ .alert, .sound, .badge]) { granted, error in + print("Permission granted: \(granted)") + if let error = error { + print("NotificationsRequestAuthorization failed with error: \(error.localizedDescription)") + } + Mindbox.shared.notificationsRequestAuthorization(granted: granted) + } + } + } +} + diff --git a/Example/Example/Assets.xcassets/AccentColor.colorset/Contents.json b/Example/Example/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 00000000..eb878970 --- /dev/null +++ b/Example/Example/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Example/Example/Assets.xcassets/AppIcon.appiconset/AppIcon1024x1024.png b/Example/Example/Assets.xcassets/AppIcon.appiconset/AppIcon1024x1024.png new file mode 100644 index 0000000000000000000000000000000000000000..236dea42e2eb1e916289e1bc04202d3e84c4e9c3 GIT binary patch literal 47711 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 MSzIl>;`aQ10o7nUB>(^b literal 0 HcmV?d00001 diff --git a/Example/Example/Assets.xcassets/AppIcon.appiconset/Contents.json b/Example/Example/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..d7ab46b9 --- /dev/null +++ b/Example/Example/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,14 @@ +{ + "images" : [ + { + "filename" : "AppIcon1024x1024.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Example/Example/Assets.xcassets/Contents.json b/Example/Example/Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/Example/Example/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Example/Example/Assets.xcassets/MBBackground.colorset/Contents.json b/Example/Example/Assets.xcassets/MBBackground.colorset/Contents.json new file mode 100644 index 00000000..f9e8ab88 --- /dev/null +++ b/Example/Example/Assets.xcassets/MBBackground.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000", + "red" : "1.000" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "0.165", + "green" : "0.176", + "red" : "0.169" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Example/Example/Assets.xcassets/MBGreen.colorset/Contents.json b/Example/Example/Assets.xcassets/MBGreen.colorset/Contents.json new file mode 100644 index 00000000..94772acc --- /dev/null +++ b/Example/Example/Assets.xcassets/MBGreen.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "0.408", + "green" : "0.808", + "red" : "0.404" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "0.408", + "green" : "0.808", + "red" : "0.404" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Example/Example/Assets.xcassets/MBText.colorset/Contents.json b/Example/Example/Assets.xcassets/MBText.colorset/Contents.json new file mode 100644 index 00000000..d8907191 --- /dev/null +++ b/Example/Example/Assets.xcassets/MBText.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.000", + "green" : "0.000", + "red" : "0.000" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000", + "red" : "1.000" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Example/Example/Assets.xcassets/MBView.colorset/Contents.json b/Example/Example/Assets.xcassets/MBView.colorset/Contents.json new file mode 100644 index 00000000..b4f722fa --- /dev/null +++ b/Example/Example/Assets.xcassets/MBView.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.870", + "green" : "0.870", + "red" : "0.870" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "display-p3", + "components" : { + "alpha" : "1.000", + "blue" : "0.239", + "green" : "0.239", + "red" : "0.239" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Example/Example/Assets.xcassets/m-green_Main_large.imageset/Contents.json b/Example/Example/Assets.xcassets/m-green_Main_large.imageset/Contents.json new file mode 100644 index 00000000..e636b3a3 --- /dev/null +++ b/Example/Example/Assets.xcassets/m-green_Main_large.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "m-green_Main_large.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "m-green_Main_large 1.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "m-green_Main_large 2.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Example/Example/Assets.xcassets/m-green_Main_large.imageset/m-green_Main_large 1.png b/Example/Example/Assets.xcassets/m-green_Main_large.imageset/m-green_Main_large 1.png new file mode 100644 index 0000000000000000000000000000000000000000..c3d1eb3060095e753c62cd5c7628d0cfe559c201 GIT binary patch literal 48197 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 + + + + aps-environment + development + com.apple.security.application-groups + + group.cloud.Mindbox.cloud.Mindbox.ReleaseExampleIos + + + diff --git a/Example/Example/Info.plist b/Example/Example/Info.plist new file mode 100644 index 00000000..866f0578 --- /dev/null +++ b/Example/Example/Info.plist @@ -0,0 +1,29 @@ + + + + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + + + + + UIBackgroundModes + + fetch + processing + remote-notification + + + diff --git a/Example/Example/LaunchScreen.storyboard b/Example/Example/LaunchScreen.storyboard new file mode 100644 index 00000000..a0192c22 --- /dev/null +++ b/Example/Example/LaunchScreen.storyboard @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Example/Example/PrivacyInfo.xcprivacy b/Example/Example/PrivacyInfo.xcprivacy new file mode 100644 index 00000000..01ce4525 --- /dev/null +++ b/Example/Example/PrivacyInfo.xcprivacy @@ -0,0 +1,48 @@ + + + + + NSPrivacyTracking + + NSPrivacyTrackingDomains + + NSPrivacyCollectedDataTypes + + + NSPrivacyCollectedDataType + NSPrivacyCollectedDataTypeOtherDiagnosticData + NSPrivacyCollectedDataTypeLinked + + NSPrivacyCollectedDataTypeTracking + + NSPrivacyCollectedDataTypePurposes + + NSPrivacyCollectedDataTypePurposeAppFunctionality + + + + NSPrivacyCollectedDataType + NSPrivacyCollectedDataTypeDeviceID + NSPrivacyCollectedDataTypeLinked + + NSPrivacyCollectedDataTypeTracking + + NSPrivacyCollectedDataTypePurposes + + NSPrivacyCollectedDataTypePurposeProductPersonalization + + + + NSPrivacyAccessedAPITypes + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryUserDefaults + NSPrivacyAccessedAPITypeReasons + + CA92.1 + + + + + diff --git a/Example/Example/SceneDelegate.swift b/Example/Example/SceneDelegate.swift new file mode 100644 index 00000000..11e0f67a --- /dev/null +++ b/Example/Example/SceneDelegate.swift @@ -0,0 +1,24 @@ +// +// SceneDelegate.swift +// Example +// +// Created by Дмитрий Ерофеев on 29.03.2024. +// + +import UIKit +import SwiftUI + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + + var window: UIWindow? + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + guard let windowScene = (scene as? UIWindowScene) else { return } + let window = UIWindow(windowScene: windowScene) + let viewModel = MainViewModel() + window.rootViewController = UIHostingController(rootView: MainView(viewModel: viewModel)) + self.window = window + window.makeKeyAndVisible() + } +} + diff --git a/Example/Example/View/CustomVIews/ButtonsView.swift b/Example/Example/View/CustomVIews/ButtonsView.swift new file mode 100644 index 00000000..117a8374 --- /dev/null +++ b/Example/Example/View/CustomVIews/ButtonsView.swift @@ -0,0 +1,54 @@ +// +// ButtonsView.swift +// Example +// +// Created by Дмитрий Ерофеев on 29.03.2024. +// + +import SwiftUI + +struct ButtonsViewline: View { + + var label: String + var action: () -> () + + var body: some View { + HStack { + Text(label) + .foregroundStyle(.mbText) + Spacer() + Button("Show In-App") { + action() + } + .frame(width: 120) + .frame(height: 30) + .background(Color.mbGreen) + .cornerRadius(10) + .tint(.white) + } + } +} + +struct ButtonsView: View { + + @ObservedObject var viewModel: MainViewModel + + var body: some View { + ZStack { + RoundedRectangle(cornerRadius: 20) + .frame(width: 350) + .frame(height: 110) + .foregroundColor(.mbView) + VStack { + ButtonsViewline(label: "Async operation") { + viewModel.showInAppWithExecuteAsyncOperation() + } + Divider() + ButtonsViewline(label: "Sync operation") { + viewModel.showInAppWithExecuteSyncOperation() + } + } + .frame(width: 310) + } + } +} diff --git a/Example/Example/View/CustomVIews/SDKDataView.swift b/Example/Example/View/CustomVIews/SDKDataView.swift new file mode 100644 index 00000000..0b168457 --- /dev/null +++ b/Example/Example/View/CustomVIews/SDKDataView.swift @@ -0,0 +1,73 @@ +// +// SDKDataView.swift +// Example +// +// Created by Дмитрий Ерофеев on 29.03.2024. +// + +import SwiftUI + +struct TitleDescrptionView: View { + + var title: String + var message: String + + var body: some View { + HStack { + VStack(alignment: .leading) { + Text(title) + .font(.system(size: 9)) + .padding(.bottom, 2) + .lineLimit(1) + .foregroundStyle(.mbText) + Text(message) + .font(.system(size: 13)) + .contextMenu { + Button { + UIPasteboard.general.string = message + } label: { + Label("Copy", systemImage: "doc.on.doc.fill") + } + } + .lineLimit(1) + .foregroundStyle(.mbText) + } + Spacer() + } + } +} + +struct SDKDataView: View { + + @ObservedObject var viewModel: MainViewModel + + var body: some View { + ZStack { + RoundedRectangle(cornerRadius: 20) + .frame(width: 350) + .frame(height: 190) + .foregroundColor(.mbView) + VStack { + HStack { + Text("Data from SDK:") + .foregroundStyle(.mbText) + .padding(.leading, -1) + .padding(.bottom, 5) + Spacer() + } + TitleDescrptionView(title: "SDK version", message: viewModel.SDKVersion) + Divider() + TitleDescrptionView(title: "APNS token", message: viewModel.APNSToken) + Divider() + TitleDescrptionView(title: "Device UUID", message: viewModel.deviceUUID) + } + .frame(width: 310) + } + } +} + +struct SDKDataView_Preview: PreviewProvider { + static var previews: some View { + SDKDataView(viewModel: MainViewModel()) + } +} diff --git a/Example/Example/View/MainView.swift b/Example/Example/View/MainView.swift new file mode 100644 index 00000000..ff8fafd9 --- /dev/null +++ b/Example/Example/View/MainView.swift @@ -0,0 +1,38 @@ +// +// MainView.swift +// Example +// +// Created by Дмитрий Ерофеев on 29.03.2024. +// + +import SwiftUI +import Foundation + +struct MainView: View { + + @ObservedObject var viewModel: MainViewModel + @State private var showingAlert = !UserDefaults.standard.bool(forKey: "ShownAlert") + + var body: some View { + ZStack { + Color.mbBackground.ignoresSafeArea() + VStack { + ButtonsView(viewModel: viewModel) + SDKDataView(viewModel: viewModel) + } + }.onAppear { + viewModel.setupData() + let alertShown = UserDefaults.standard.bool(forKey: "ShownAlert") + if !alertShown { + UserDefaults.standard.set(true, forKey: "ShownAlert") + } + } + .alert("In-App can only be shown once per session", isPresented: $showingAlert) { + Button("OK", role: .cancel) {} + } + } +} + +#Preview { + MainView(viewModel: MainViewModel()) +} diff --git a/Example/Example/ViewModel/ChooseInAppMessagesDelegate.swift b/Example/Example/ViewModel/ChooseInAppMessagesDelegate.swift new file mode 100644 index 00000000..e81d2526 --- /dev/null +++ b/Example/Example/ViewModel/ChooseInAppMessagesDelegate.swift @@ -0,0 +1,45 @@ +// +// ChooseInAppMessagesDelegate.swift +// Example +// +// Created by Дмитрий Ерофеев on 02.04.2024. +// + +import Foundation +import Mindbox + +class ChooseInAppMessagesDelegate: InAppMessagesDelegate { + + private init() {} + static var shared = ChooseInAppMessagesDelegate() + + //https://developers.mindbox.ru/docs/in-app + func inAppMessageTapAction(id: String, url: URL?, payload: String) { + //Here you can add your custom logic + print("inAppMessageTapAction") + print(id) + print(url ?? "") + print(payload) + } + + //https://developers.mindbox.ru/docs/in-app + func inAppMessageDismissed(id: String) { + //Here you can add your custom logic + print("inAppMessageDismissed") + print(id) + } + + func select(chooseInappMessageDelegate: ChooseInappMessageDelegate) { + switch chooseInappMessageDelegate { + case .DefaultInappMessageDelegate: + break + case .InAppMessagesDelegate: + Mindbox.shared.inAppMessagesDelegate = self + } + } +} + +enum ChooseInappMessageDelegate { + case DefaultInappMessageDelegate + case InAppMessagesDelegate +} diff --git a/Example/Example/ViewModel/MainViewModel.swift b/Example/Example/ViewModel/MainViewModel.swift new file mode 100644 index 00000000..88586fc1 --- /dev/null +++ b/Example/Example/ViewModel/MainViewModel.swift @@ -0,0 +1,72 @@ +// +// MainViewModel.swift +// Example +// +// Created by Дмитрий Ерофеев on 29.03.2024. +// + +import Foundation +import Mindbox +import UIKit + +class MainViewModel: ObservableObject { + + @Published var SDKVersion: String = "" + @Published var deviceUUID: String = "" + @Published var APNSToken: String = "" + + //https://developers.mindbox.ru/docs/ios-sdk-methods + func setupData() { + self.SDKVersion = Mindbox.shared.sdkVersion + Mindbox.shared.getDeviceUUID { deviceUUID in + DispatchQueue.main.async { + self.deviceUUID = deviceUUID + } + } + Mindbox.shared.getAPNSToken { APNSToken in + DispatchQueue.main.async { + self.APNSToken = APNSToken + } + } + ChooseInAppMessagesDelegate.shared.select(chooseInappMessageDelegate: .InAppMessagesDelegate) + } + + //https://developers.mindbox.ru/docs/in-app-targeting-by-custom-operation + func showInAppWithExecuteSyncOperation () { + let json = """ + { "viewProduct": + { "product": + { "ids": + { "website": "1" } + } + } + } + """ + Mindbox.shared.executeSyncOperation(operationSystemName: "APIMethodForReleaseExampleIos", json: json) { result in + switch result { + case .success(let success): + Mindbox.logger.log(level: .info, message: "\(success)") + case .failure(let error): + Mindbox.logger.log(level: .error, message: "\(error)") + } + } + } + + //https://developers.mindbox.ru/docs/in-app-targeting-by-custom-operation + func showInAppWithExecuteAsyncOperation () { + let json = """ + { "viewProduct": + { "product": + { "ids": + { "website": "2" } + } + } + } + """ + Mindbox.shared.executeAsyncOperation(operationSystemName: "APIMethodForReleaseExampleIos", json: json) + } +} + + + + diff --git a/Example/MindboxNotificationContentExtension/Info.plist b/Example/MindboxNotificationContentExtension/Info.plist new file mode 100644 index 00000000..b9111b15 --- /dev/null +++ b/Example/MindboxNotificationContentExtension/Info.plist @@ -0,0 +1,22 @@ + + + + + NSExtension + + NSExtensionPrincipalClass + MindboxNotificationContentExtension.NotificationViewController + NSExtensionAttributes + + UNNotificationExtensionUserInteractionEnabled + 1 + UNNotificationExtensionCategory + MindBoxCategoryIdentifier + UNNotificationExtensionInitialContentSizeRatio + 0.0001 + + NSExtensionPointIdentifier + com.apple.usernotifications.content-extension + + + diff --git a/Example/MindboxNotificationContentExtension/MindboxNotificationContentExtension.entitlements b/Example/MindboxNotificationContentExtension/MindboxNotificationContentExtension.entitlements new file mode 100644 index 00000000..ddec1215 --- /dev/null +++ b/Example/MindboxNotificationContentExtension/MindboxNotificationContentExtension.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.cloud.Mindbox.cloud.Mindbox.ReleaseExampleIos + + + diff --git a/Example/MindboxNotificationContentExtension/NotificationViewController.swift b/Example/MindboxNotificationContentExtension/NotificationViewController.swift new file mode 100644 index 00000000..b45dc0ce --- /dev/null +++ b/Example/MindboxNotificationContentExtension/NotificationViewController.swift @@ -0,0 +1,20 @@ +// +// NotificationViewController.swift +// MindboxNotificationContentExtension +// +// Created by Дмитрий Ерофеев on 31.03.2024. +// + +import UIKit +import UserNotifications +import UserNotificationsUI +import MindboxNotifications + +class NotificationViewController: UIViewController, UNNotificationContentExtension { + + lazy var mindboxService = MindboxNotificationService() + + func didReceive(_ notification: UNNotification) { + mindboxService.didReceive(notification: notification, viewController: self, extensionContext: extensionContext) + } +} diff --git a/Example/MindboxNotificationServiceExtension/Info.plist b/Example/MindboxNotificationServiceExtension/Info.plist new file mode 100644 index 00000000..57421ebf --- /dev/null +++ b/Example/MindboxNotificationServiceExtension/Info.plist @@ -0,0 +1,13 @@ + + + + + NSExtension + + NSExtensionPointIdentifier + com.apple.usernotifications.service + NSExtensionPrincipalClass + $(PRODUCT_MODULE_NAME).NotificationService + + + diff --git a/Example/MindboxNotificationServiceExtension/MindboxNotificationServiceExtension.entitlements b/Example/MindboxNotificationServiceExtension/MindboxNotificationServiceExtension.entitlements new file mode 100644 index 00000000..ddec1215 --- /dev/null +++ b/Example/MindboxNotificationServiceExtension/MindboxNotificationServiceExtension.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.cloud.Mindbox.cloud.Mindbox.ReleaseExampleIos + + + diff --git a/Example/MindboxNotificationServiceExtension/NotificationService.swift b/Example/MindboxNotificationServiceExtension/NotificationService.swift new file mode 100644 index 00000000..2fa604c4 --- /dev/null +++ b/Example/MindboxNotificationServiceExtension/NotificationService.swift @@ -0,0 +1,24 @@ +// +// NotificationService.swift +// MindboxNotificationServiceExtension +// +// Created by Дмитрий Ерофеев on 30.03.2024. +// + +import UserNotifications +import MindboxNotifications + +class NotificationService: UNNotificationServiceExtension { + + lazy var mindboxService = MindboxNotificationService() + + override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { + mindboxService.didReceive(request, withContentHandler: contentHandler) + } + + override func serviceExtensionTimeWillExpire() { + // Called just before the extension will be terminated by the system. + // Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used. + mindboxService.serviceExtensionTimeWillExpire() + } +} diff --git a/Example/Podfile b/Example/Podfile new file mode 100644 index 00000000..3f3c6553 --- /dev/null +++ b/Example/Podfile @@ -0,0 +1,13 @@ +use_frameworks! + +target 'Example' do + pod 'Mindbox', '2.8.5' +end + +target 'MindboxNotificationServiceExtension' do + pod 'MindboxNotifications', '2.8.5' +end + +target 'MindboxNotificationContentExtension' do + pod 'MindboxNotifications', '2.8.5' +end \ No newline at end of file diff --git a/Example/Podfile.lock b/Example/Podfile.lock new file mode 100644 index 00000000..0168f173 --- /dev/null +++ b/Example/Podfile.lock @@ -0,0 +1,25 @@ +PODS: + - Mindbox (2.8.5): + - MindboxLogger (= 0.0.6) + - MindboxLogger (0.0.6) + - MindboxNotifications (2.8.5): + - MindboxLogger (= 0.0.6) + +DEPENDENCIES: + - Mindbox (= 2.8.5) + - MindboxNotifications (= 2.8.5) + +SPEC REPOS: + trunk: + - Mindbox + - MindboxLogger + - MindboxNotifications + +SPEC CHECKSUMS: + Mindbox: ff0cf579406343e606ade907e66f75cdc19e8cb6 + MindboxLogger: d59b32ee8ee008c848fe33e5862b74bac3b327bd + MindboxNotifications: f0f687e2305c2d30fd09f2c9a6f821a3ee8e2b66 + +PODFILE CHECKSUM: 1dc0b8379ca779d2bf2710b2fa281ebcfd84a4ec + +COCOAPODS: 1.15.2 diff --git a/Example/README.md b/Example/README.md new file mode 100644 index 00000000..a3fef6a6 --- /dev/null +++ b/Example/README.md @@ -0,0 +1,65 @@ +# Example app with SPM for Mindbox SDK for iOS + +This is an example of SDK [integration](https://developers.mindbox.ru/docs/ios-sdk-integration) + +## Getting started + +### Launching the application +The app has integration via cocoapods, but you can use SPM if you need. +#### Cocoapods: +1. [Clone ios-sdk repository](https://github.com/mindbox-cloud/ios-sdk). +2. Make sure you have CocoaPods installed or install it according to the instructions. +3. Go to `ios-sdk/Example/` +4. Install the pods. + ```ruby + pod update + ``` + Or + ```ruby + pod install + ``` +5. Go to `ios-sdk/Example/Example.xcworkspace`. +6. Run file `Example.xcworkspace`. +#### SPM: +1. To deintegrate cocoapods you can use next comands: + - `sudo gem install cocoapods-deintegrate` + - `pod deintegrate` + - `rm Podfile` + - `rm Podfile.lock` + - `rm Example.xcworkspace` + - `rm -rf Pods` +2. Launch `Example.xcodeproj`. +3. [Read this](https://developers.mindbox.ru/docs/add-ios-sdk) and follow the initialization instructions via SPM. + +Now you can test the in-app on the simulator. +In our admin panel there are already 3 ready-made in-apps that you can look at. +To run the application on a real device and try push notifications, follow the instructions below. + +### Setting up a Example application with your personal account (to run on a real device) + +1. Change [team](https://developers.mindbox.ru/docs/ios-get-keys) and bundle identifiers and App Group name for next targets: + - ExampleApp + - MindboxNotificationServiceExtension + - MindboxNotificationContentExtension +2. [Configure your endpoints](https://developers.mindbox.ru/docs/add-ios-integration). +3. Change domain and endpoints in the `AppDelegate.swift` to yours. + +### SDK functionality testing + +1. To check innap when opening: + - [Read this](https://help.mindbox.ru/docs/in-app-what-is). + - Open app. +2. To check the inapp anywhere in the application: + - [Read this](https://help.mindbox.ru/docs/in-app-location). + - Replace `operationSystemName` in `showInAppWithExecuteSyncOperation` and `showInAppWithExecuteAsyncOperation` in MainViewModel. + - Click to the button `Show in-app` opposite the selected operation. +3. To check push notifications: + - [Read this](https://developers.mindbox.ru/docs/ios-send-push-notifications-advanced) + - Send a notification from your account. +4. To check rich notifications: + - [Read this](https://developers.mindbox.ru/docs/ios-send-push-notifications-advanced) + - Send a notification from your account. + +### Additionally + - Currently the In-App only comes once per session. + - There are comments and links in the ExampleApp code that can help you. From 6a2a3d1822183b5e220fb254281f8b79450c689e Mon Sep 17 00:00:00 2001 From: Akylbek Utekeshev Date: Thu, 4 Apr 2024 18:40:05 +0500 Subject: [PATCH 14/35] MBX-3264 Notify to all operations (including alreadyshown) --- .../InAppConfigurationMapper/InAppConfigutationMapper.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Mindbox/InAppMessages/InAppConfigurationMapper/InAppConfigutationMapper.swift b/Mindbox/InAppMessages/InAppConfigurationMapper/InAppConfigutationMapper.swift index 70d1a7d5..438fd03a 100644 --- a/Mindbox/InAppMessages/InAppConfigurationMapper/InAppConfigutationMapper.swift +++ b/Mindbox/InAppMessages/InAppConfigurationMapper/InAppConfigutationMapper.swift @@ -52,10 +52,11 @@ final class InAppConfigutationMapper: InAppConfigurationMapperProtocol { targetingChecker.event = event prepareTargetingChecker(for: filteredInapps) - dataFacade.setObservedOperation() prepareForRemainingTargeting() + dataFacade.setObservedOperation() + if filteredInapps.isEmpty { Logger.common(message: "No inapps to show", level: .debug, category: .inAppMessages) completion(nil) From 2a4e1229341312512add6ae91ecb842ee5490bf7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 4 Apr 2024 22:53:48 +0000 Subject: [PATCH 15/35] Update dependency fastlane to v2.220.0 --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 207ca45e..29f25b52 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -19,7 +19,7 @@ GEM artifactory (3.0.17) atomos (0.1.3) aws-eventstream (1.3.0) - aws-partitions (1.907.0) + aws-partitions (1.908.0) aws-sdk-core (3.191.6) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.651.0) From ce107b4265bb5dc23f22bab76e3462002bca3b19 Mon Sep 17 00:00:00 2001 From: Akylbek Utekeshev Date: Fri, 5 Apr 2024 13:00:28 +0500 Subject: [PATCH 16/35] MBX-0000 Updated Podspecs --- Mindbox.podspec | 2 +- MindboxLogger.podspec | 2 +- MindboxNotifications.podspec | 3 +++ 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Mindbox.podspec b/Mindbox.podspec index caeebb63..d35e19c2 100644 --- a/Mindbox.podspec +++ b/Mindbox.podspec @@ -11,7 +11,7 @@ Pod::Spec.new do |spec| spec.source_files = "Mindbox/**/*.{swift}", "SDKVersionProvider/**/*.{swift}" spec.exclude_files = "Classes/Exclude" spec.resource_bundles = { - 'Mindbox' => ['Mindbox/**/*.xcassets', 'Mindbox/**/*.xcdatamodeld'] + 'Mindbox' => ['Mindbox/**/*.xcassets', 'Mindbox/**/*.xcdatamodeld', 'Mindbox/**/*.xcprivacy'] } spec.swift_version = "5" spec.dependency 'MindboxLogger', '0.0.6' diff --git a/MindboxLogger.podspec b/MindboxLogger.podspec index 1d9b55eb..c32e7754 100644 --- a/MindboxLogger.podspec +++ b/MindboxLogger.podspec @@ -11,7 +11,7 @@ Pod::Spec.new do |spec| spec.source_files = "MindboxLogger/**/*.{swift}", "SDKVersionProvider/**/*.{swift}" spec.exclude_files = "Classes/Exclude" spec.resource_bundles = { - 'MindboxLogger' => ["MindboxLogger/**/*.xcdatamodeld"] + 'MindboxLogger' => ["MindboxLogger/**/*.xcdatamodeld", 'MindboxLogger/**/*.xcprivacy'] } spec.swift_version = "5" diff --git a/MindboxNotifications.podspec b/MindboxNotifications.podspec index fb7c8737..ab1f95f5 100644 --- a/MindboxNotifications.podspec +++ b/MindboxNotifications.podspec @@ -10,6 +10,9 @@ Pod::Spec.new do |spec| spec.source = { :git => "https://github.com/mindbox-cloud/ios-sdk.git", :tag => spec.version } spec.source_files = "MindboxNotifications/**/*.{swift}", "SDKVersionProvider/**/*.{swift}" spec.exclude_files = "Classes/Exclude" + spec.resource_bundles = { + 'MindboxNotifications' => ['MindboxNotifications/**/*.xcprivacy'] + } spec.swift_version = "5" spec.dependency 'MindboxLogger', '0.0.6' From 046d21e2df9be12036a8623e3d982ab0b4ab1bf2 Mon Sep 17 00:00:00 2001 From: Akylbek Utekeshev Date: Fri, 5 Apr 2024 13:01:05 +0500 Subject: [PATCH 17/35] MBX-0000 MindboxLogger version UP --- Mindbox.podspec | 2 +- MindboxLogger.podspec | 2 +- MindboxNotifications.podspec | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Mindbox.podspec b/Mindbox.podspec index d35e19c2..f1937caf 100644 --- a/Mindbox.podspec +++ b/Mindbox.podspec @@ -14,6 +14,6 @@ Pod::Spec.new do |spec| 'Mindbox' => ['Mindbox/**/*.xcassets', 'Mindbox/**/*.xcdatamodeld', 'Mindbox/**/*.xcprivacy'] } spec.swift_version = "5" - spec.dependency 'MindboxLogger', '0.0.6' + spec.dependency 'MindboxLogger', '0.0.7' end diff --git a/MindboxLogger.podspec b/MindboxLogger.podspec index c32e7754..10dbe2a1 100644 --- a/MindboxLogger.podspec +++ b/MindboxLogger.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |spec| spec.name = "MindboxLogger" - spec.version = "0.0.6" + spec.version = "0.0.7" spec.summary = "SDK for utilities to work with Mindbox" spec.description = "-" spec.homepage = "https://github.com/mindbox-cloud/ios-sdk" diff --git a/MindboxNotifications.podspec b/MindboxNotifications.podspec index ab1f95f5..e74ef05b 100644 --- a/MindboxNotifications.podspec +++ b/MindboxNotifications.podspec @@ -14,6 +14,6 @@ Pod::Spec.new do |spec| 'MindboxNotifications' => ['MindboxNotifications/**/*.xcprivacy'] } spec.swift_version = "5" - spec.dependency 'MindboxLogger', '0.0.6' + spec.dependency 'MindboxLogger', '0.0.7' end From 3b5de93f57d2ad02809c7adbec5c9c57977ac09d Mon Sep 17 00:00:00 2001 From: Dmitry Erofeev <56806136+kniksees@users.noreply.github.com> Date: Mon, 8 Apr 2024 11:45:52 +0300 Subject: [PATCH 18/35] MBX-3233: add logs to MindboxNotifications (#312) * feature/MBX-3233: add logs to BodyDecoder * feature/MBX-3233: add logs to BodyEncoder * feature/MBX-3233: update logs in BodyDecoder * feature/MBX-3233: add logs to ImageFormat * feature/MBX-3233: add logs to URLRequestBuilder * feature/MBX-3233: add logs to NotificationDecoder * feature/MBX-3233: add logs to DeviceModelHelper * feature/MBX-3233: add logs to DeliveryService * feature/MBX-3233: update logs in DeviceModelHelper * feature/MBX-3233: add logs to MBConfiguration * feature/MBX-3233: add logs to MBUtilitiesFetcher * feature/MBX-3233: update logs in BodyDecoder * feature/MBX-3233: update logs in BodyEncoder * feature/MBX-3233: change comment language in Event * feature/MBX-3233: update logs ImageFormat * feature/MBX-3233: update logs in NotificationDecoder * feature/MBX-3233: update logs in URLRequestBuilder * feature/MBX-3233: update logs in NetworkService * feature/MBX-3233: add logs to MBUtilitiesFetcher and remove fatalError from MBUtilitiesFetcher * feature/MBX-3233: add logs to DeliveryOperation * feature/MBX-3233: update logs in NotificationDecoder * feature/MBX-3233: update logs in MindboxNotificationService * feature/MBX-3233: refactor MBConfiguration * feature/MBX-3233: refactor MindboxNotificationService * feature/MBX-3233: update logs in MBConfiguration * feature/MBX-3233: update logs in DeviceModelHelper * feature/MBX-3233: update logs in MBConfiguration * feature/MBX-3233: update logs in MBUtilitiesFetcher * feature/MBX-3233: update logs in BodyDecoder * feature/MBX-3233: update logs in BodyEncoder * feature/MBX-3233: update logs in NetworkService * feature/MBX-3233: update logs in ImageFormat * feature/MBX-3233: update logs in NotificationDecoder * feature/MBX-3233: update logs in URLRequestBuilder * feature/MBX-3233: spaces removed from MindboxNotificationService * feature/MBX-3233: spaces removed from MindboxNotificationService * feature/MBX-3233: update logs in NetworkService * feature/MBX-3233: update logs in PushDeliveryOperation * feature/MBX-3233: update logs in MindboxNotificationService * feature/MBX-3233: update logs in PushDeliveryOperation * Revert "feature/MBX-3233: update logs in NetworkService" * feature/MBX-3233: update logs in NetworkService * feature/MBX-3233: update logs in DeviceModelHelper * feature/MBX-3233: removed logs from MBConfiguration * feature/MBX-3233: update logs in MindboxNotificationService * feature/MBX-3233: update logs in MBUtilitiesFetcher --- .../MindboxNotificationService.swift | 54 ++++++++++++++----- .../Network/DeliveryService.swift | 2 + .../Network/DeviceModelHelper.swift | 5 +- .../Network/MBConfiguration.swift | 18 +++++-- .../Network/MBUtilitiesFetcher.swift | 20 ++++--- .../Network/Model/Body+/BodyDecoder.swift | 3 ++ .../Network/Model/Body+/BodyEncoder.swift | 2 + .../Network/Model/DeliveryOperation.swift | 6 ++- .../Network/Model/Event.swift | 2 +- .../Network/Model/ImageFormat.swift | 7 ++- .../Network/Model/NotificationDecoder.swift | 11 ++-- .../Network/Model/URLRequestBuilder.swift | 2 + .../Network/NetworkService.swift | 1 + 13 files changed, 103 insertions(+), 30 deletions(-) diff --git a/MindboxNotifications/MindboxNotificationService.swift b/MindboxNotifications/MindboxNotificationService.swift index 2a59e20b..d8d38442 100644 --- a/MindboxNotifications/MindboxNotificationService.swift +++ b/MindboxNotifications/MindboxNotificationService.swift @@ -40,10 +40,13 @@ public class MindboxNotificationService: NSObject { public func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { self.contentHandler = contentHandler bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent) - guard let bestAttemptContent = bestAttemptContent else { return } + guard let bestAttemptContent = bestAttemptContent else { + Logger.common(message: "MindboxNotificationService: Failed to get bestAttemptContent. bestAttemptContent: \(String(describing: bestAttemptContent))", level: .error, category: .notification) + return + } pushDelivered(request) - + Logger.common(message: "Push notification UniqueKey: \(request.identifier)", level: .info, category: .notification) if let imageUrl = parse(request: request)?.withImageURL?.imageUrl, @@ -53,23 +56,26 @@ public class MindboxNotificationService: NSObject { self?.proceedFinalStage(bestAttemptContent) } } else { + Logger.common(message: "MindboxNotificationService: Failed to parse imageUrl", level: .error, category: .notification) proceedFinalStage(bestAttemptContent) } } - + /// Call this method in `didReceive(_ request, withContentHandler)` of your `NotificationService` if you have implemented a custom version of NotificationService. This is necessary as an indicator that the push notification has been delivered to Mindbox services. public func pushDelivered(_ request: UNNotificationRequest) { let utilities = MBUtilitiesFetcher() guard let configuration = utilities.configuration else { - Logger.error(.init(errorType: .internal, description: "Configuration is not set")) + Logger.common(message: "MindboxNotificationService: Failed to get configuration", level: .error, category: .notification) return } + Logger.common(message: "MindboxNotificationService: Successfully received configuration. configuration: \(configuration)", level: .info, category: .notification) let networkService = NetworkService(utilitiesFetcher: utilities, configuration: configuration) let deliveryService = DeliveryService(utilitiesFetcher: utilities, networkService: networkService) - + do { try deliveryService.track(request: request) + Logger.common(message: "MindboxNotificationService: Successfully tracked. request: \(request)", level: .info, category: .notification) } catch { Logger.error(.init(errorType: .unknown, description: error.localizedDescription)) } @@ -81,11 +87,12 @@ public class MindboxNotificationService: NSObject { defer { completion() } guard let self = self, let data = data else { + Logger.common(message: "MindboxNotificationService: Failed to get self or data. self: \(String(describing: self)), data: \(String(describing: data))", level: .error, category: .notification) return } - - Logger.response(data: data, response: response, error: error) + Logger.response(data: data, response: response, error: error) + if let attachment = self.saveImage(data) { self.bestAttemptContent?.attachments = [attachment] } @@ -100,13 +107,17 @@ public class MindboxNotificationService: NSObject { /// Call this method in `serviceExtensionTimeWillExpire()` of `NotificationService` public func serviceExtensionTimeWillExpire() { if let bestAttemptContent = bestAttemptContent { + Logger.common(message: "MindboxNotificationService: Failed to get bestAttemptContent. bestAttemptContent: \(bestAttemptContent)", level: .error, category: .notification) proceedFinalStage(bestAttemptContent) } } private func createContent(for notification: UNNotification, extensionContext: NSExtensionContext?) { let request = notification.request - guard let payload = parse(request: request) else { return } + guard let payload = parse(request: request) else { + Logger.common(message: "MindboxNotificationService: Failed to parse payload. request: \(request)", level: .error, category: .notification) + return + } if let attachment = notification.request.content.attachments.first, attachment.url.startAccessingSecurityScopedResource() { @@ -117,6 +128,7 @@ public class MindboxNotificationService: NSObject { private func createActions(with payload: Payload, context: NSExtensionContext?) { guard let context = context, let buttons = payload.withButton?.buttons else { + Logger.common(message: "MindboxNotificationService: Failed to create actions. payload: \(payload), context: \(String(describing: context)), payload.withButton?.buttons: \(String(describing: payload.withButton?.buttons))", level: .error, category: .notification) return } let actions = buttons.map { button in @@ -140,7 +152,10 @@ public class MindboxNotificationService: NSObject { private func createImageView(with imagePath: String, view: UIView?) { guard let view = view, - let data = FileManager.default.contents(atPath: imagePath) else { return } + let data = FileManager.default.contents(atPath: imagePath) else { + Logger.common(message: "MindboxNotificationService: Failed to create view. imagePath: \(imagePath), view: \(String(describing: view))", level: .error, category: .notification) + return + } let imageView = UIImageView(image: UIImage(data: data)) imageView.contentMode = .scaleAspectFill @@ -157,24 +172,36 @@ public class MindboxNotificationService: NSObject { } private func parse(request: UNNotificationRequest) -> Payload? { - guard let userInfo = getUserInfo(from: request) else { return nil } - guard let data = try? JSONSerialization.data(withJSONObject: userInfo, options: .prettyPrinted) else { return nil } + guard let userInfo = getUserInfo(from: request) else { + Logger.common(message: "MindboxNotificationService: Failed to get userInfo", level: .error, category: .notification) + return nil + } + guard let data = try? JSONSerialization.data(withJSONObject: userInfo, options: .prettyPrinted) else { + Logger.common(message: "MindboxNotificationService: Failed to get data. userInfo: \(userInfo)", level: .error, category: .notification) + return nil + } var payload = Payload() payload.withButton = try? JSONDecoder().decode(Payload.Button.self, from: data) + Logger.common(message: "MindboxNotificationService: payload.withButton: \(String(describing: payload.withButton))", level: .info, category: .notification) + payload.withImageURL = try? JSONDecoder().decode(Payload.ImageURL.self, from: data) - + Logger.common(message: "MindboxNotificationService: payload.withImageURL: \(String(describing: payload.withImageURL))", level: .info, category: .notification) + return payload } private func getUserInfo(from request: UNNotificationRequest) -> [AnyHashable: Any]? { guard let userInfo = (request.content.mutableCopy() as? UNMutableNotificationContent)?.userInfo else { + Logger.common(message: "MindboxNotificationService: Failed to get userInfo", level: .error, category: .notification) return nil } if userInfo.keys.count == 1, let innerUserInfo = userInfo["aps"] as? [AnyHashable: Any] { + Logger.common(message: "MindboxNotificationService: userInfo: \(innerUserInfo), userInfo.keys.count: \(userInfo.keys.count), innerUserInfo: \(innerUserInfo)", level: .info, category: .notification) return innerUserInfo } else { + Logger.common(message: "MindboxNotificationService: userInfo: \(userInfo)", level: .info, category: .notification) return userInfo } } @@ -182,7 +209,7 @@ public class MindboxNotificationService: NSObject { private func saveImage(_ data: Data) -> UNNotificationAttachment? { let name = UUID().uuidString guard let format = ImageFormat(data) else { - Logger.common(message: "Image load failed", level: .error, category: .notification) + Logger.common(message: "MindboxNotificationService: Image load failed, data: \(data)", level: .error, category: .notification) return nil } let url = URL(fileURLWithPath: NSTemporaryDirectory()) @@ -193,6 +220,7 @@ public class MindboxNotificationService: NSObject { try data.write(to: fileURL, options: .atomic) return try UNNotificationAttachment(identifier: name, url: fileURL, options: nil) } catch { + Logger.common(message: "MindboxNotificationService: Failed to save image. data: \(data), name: \(name), url: \(url), directory: \(directory)", level: .error, category: .notification) return nil } } diff --git a/MindboxNotifications/Network/DeliveryService.swift b/MindboxNotifications/Network/DeliveryService.swift index 1749e560..8bdfecc2 100644 --- a/MindboxNotifications/Network/DeliveryService.swift +++ b/MindboxNotifications/Network/DeliveryService.swift @@ -42,9 +42,11 @@ public class DeliveryService { @discardableResult func track(request: UNNotificationRequest) throws -> Bool { guard let decoder = NotificationDecoder(request: request) else { + Logger.common(message: "DeliveryService: Failed to create decoder. request: \(request)", level: .error, category: .notification) return false } guard decoder.isMindboxNotification else { + Logger.common(message: "DeliveryService: Not MindboxNotification. decoder.isMindboxNotification: \(decoder.isMindboxNotification)", level: .error, category: .notification) return false } let payload = try decoder.decode() diff --git a/MindboxNotifications/Network/DeviceModelHelper.swift b/MindboxNotifications/Network/DeviceModelHelper.swift index 13015332..ebb745e8 100644 --- a/MindboxNotifications/Network/DeviceModelHelper.swift +++ b/MindboxNotifications/Network/DeviceModelHelper.swift @@ -8,6 +8,7 @@ import Foundation import UIKit.UIDevice +import MindboxLogger struct DeviceModelHelper { @@ -19,7 +20,9 @@ struct DeviceModelHelper { uname(&systemInfo) let machineMirror = Mirror(reflecting: systemInfo.machine) let identifier = machineMirror.children.reduce("") { identifier, element in - guard let value = element.value as? Int8, value != 0 else { return identifier } + guard let value = element.value as? Int8, value != 0 else { + return identifier + } return identifier + String(UnicodeScalar(UInt8(value))) } return identifier diff --git a/MindboxNotifications/Network/MBConfiguration.swift b/MindboxNotifications/Network/MBConfiguration.swift index 7ee44aab..4dbb8e10 100644 --- a/MindboxNotifications/Network/MBConfiguration.swift +++ b/MindboxNotifications/Network/MBConfiguration.swift @@ -8,6 +8,7 @@ import Foundation +import MindboxLogger public struct MBConfiguration: Codable { public let endpoint: String @@ -27,11 +28,18 @@ public struct MBConfiguration: Codable { } } - guard let url = findeURL else { return nil } + guard let url = findeURL else { + return nil + } + + guard let data = try? Data(contentsOf: url) else { + return nil + } - guard let data = try? Data(contentsOf: url) else { return nil } + guard let configuration = try? decoder.decode(MBConfiguration.self, from: data) else { + return nil + } - guard let configuration = try? decoder.decode(MBConfiguration.self, from: data) else { return nil } self = configuration } @@ -48,17 +56,21 @@ public struct MBConfiguration: Codable { let endpoint = try values.decode(String.self, forKey: .endpoint) let domain = try values.decode(String.self, forKey: .domain) var previousInstallationId: String? + if let value = try? values.decode(String.self, forKey: .previousInstallationId) { if !value.isEmpty { previousInstallationId = value } } + var previousDeviceUUID: String? + if let value = try? values.decode(String.self, forKey: .previousDeviceUUID) { if !value.isEmpty { previousDeviceUUID = value } } + let subscribeCustomerIfCreated = try values.decodeIfPresent(Bool.self, forKey: .subscribeCustomerIfCreated) ?? false self.endpoint = endpoint diff --git a/MindboxNotifications/Network/MBUtilitiesFetcher.swift b/MindboxNotifications/Network/MBUtilitiesFetcher.swift index cca941bb..1ecfd31f 100644 --- a/MindboxNotifications/Network/MBUtilitiesFetcher.swift +++ b/MindboxNotifications/Network/MBUtilitiesFetcher.swift @@ -1,5 +1,5 @@ // -// UtilitiesFetcher.swift +// MBUtilitiesFetcher.swift // MindboxNotifications // // Created by Ihor Kandaurov on 22.06.2021. @@ -7,6 +7,7 @@ // import Foundation +import MindboxLogger #if SWIFT_PACKAGE import SDKVersionProvider #endif @@ -23,10 +24,11 @@ class MBUtilitiesFetcher { var bundle = Bundle(for: MindboxNotificationService.self) return bundle }() - - var applicationGroupIdentifier: String { + + var applicationGroupIdentifier: String? { guard let hostApplicationName = hostApplicationName else { - fatalError("CFBundleShortVersionString not found for host app") + Logger.common(message: "MBUtilitiesFetcher: Failed to get applicationGroupIdentifier. hostApplicationName: \(String(describing: hostApplicationName))", level: .error, category: .notification) + return nil } let identifier = "group.cloud.Mindbox.\(hostApplicationName)" let url = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: identifier) @@ -34,8 +36,8 @@ class MBUtilitiesFetcher { #if targetEnvironment(simulator) return "" #else - let message = "AppGroup for \(hostApplicationName) not found. Add AppGroup with value: \(identifier)" - fatalError(message) + Logger.common(message: "MBUtilitiesFetcher: Failed to get AppGroup for \(hostApplicationName). identifier: \(identifier))", level: .error, category: .notification) + return nil #endif } return identifier @@ -51,6 +53,7 @@ class MBUtilitiesFetcher { let url = bundle.bundleURL.deletingLastPathComponent().deletingLastPathComponent() if let otherBundle = Bundle(url: url) { bundle = otherBundle + Logger.common(message: "MBUtilitiesFetcher: Successfully prepared bundle. bundle: \(bundle)", level: .debug, category: .notification) } } } @@ -72,7 +75,10 @@ class MBUtilitiesFetcher { } var configuration: MBConfiguration? { - guard let data = userDefaults?.data(forKey: "MBPersistenceStorage-configurationData") else { return nil } + guard let data = userDefaults?.data(forKey: "MBPersistenceStorage-configurationData") else { + Logger.common(message: "MBUtilitiesFetcher: Failed to get data from userDefaults for key 'MBPersistenceStorage-configurationData'", level: .error, category: .notification) + return nil + } return try? JSONDecoder().decode(MBConfiguration.self, from: data) } } diff --git a/MindboxNotifications/Network/Model/Body+/BodyDecoder.swift b/MindboxNotifications/Network/Model/Body+/BodyDecoder.swift index 23a6b20b..d134eb91 100644 --- a/MindboxNotifications/Network/Model/Body+/BodyDecoder.swift +++ b/MindboxNotifications/Network/Model/Body+/BodyDecoder.swift @@ -7,6 +7,7 @@ // import Foundation +import MindboxLogger struct BodyDecoder { @@ -17,9 +18,11 @@ struct BodyDecoder { if let body = try? JSONDecoder().decode(T.self, from: data) { self.body = body } else { + Logger.common(message: "BodyDecoder: Failed to decode JSON. data: \(data)", level: .error, category: .notification) return nil } } else { + Logger.common(message: "BodyDecoder: Failed to decode string. decodable: \(decodable)", level: .error, category: .notification) return nil } } diff --git a/MindboxNotifications/Network/Model/Body+/BodyEncoder.swift b/MindboxNotifications/Network/Model/Body+/BodyEncoder.swift index 8b28539f..18966d69 100644 --- a/MindboxNotifications/Network/Model/Body+/BodyEncoder.swift +++ b/MindboxNotifications/Network/Model/Body+/BodyEncoder.swift @@ -7,6 +7,7 @@ // import Foundation +import MindboxLogger struct BodyEncoder { @@ -17,6 +18,7 @@ struct BodyEncoder { body = String(data: encodedBody, encoding: .utf8) ?? "" } else { body = "" + Logger.common(message: "BodyEncoder: Failed to encode JSON. encodable: \(encodable)", level: .error, category: .notification) } } diff --git a/MindboxNotifications/Network/Model/DeliveryOperation.swift b/MindboxNotifications/Network/Model/DeliveryOperation.swift index 760f9a77..be2350d3 100644 --- a/MindboxNotifications/Network/Model/DeliveryOperation.swift +++ b/MindboxNotifications/Network/Model/DeliveryOperation.swift @@ -7,6 +7,7 @@ // import Foundation +import MindboxLogger class PushDeliveryOperation: Operation { private let event: Event @@ -43,7 +44,10 @@ class PushDeliveryOperation: Operation { } service.sendPushDelivered(event: event) { [weak self] result in - guard let self = self else { return } + guard let self = self else { + Logger.common(message: "PushDeliveryOperation: Failed to get PushDeliveryOperation. self: \(String(describing: self))", level: .error, category: .notification) + return + } switch result { case true: self.onCompleted?(self.event, true) diff --git a/MindboxNotifications/Network/Model/Event.swift b/MindboxNotifications/Network/Model/Event.swift index 80e24d69..7089f188 100644 --- a/MindboxNotifications/Network/Model/Event.swift +++ b/MindboxNotifications/Network/Model/Event.swift @@ -22,7 +22,7 @@ struct Event { return Int64(ms) } - // Время добавляения персистентно в очередь событий + // The time of adding is persistent to the event queue let enqueueTimeStamp: Double let serialNumber: String? diff --git a/MindboxNotifications/Network/Model/ImageFormat.swift b/MindboxNotifications/Network/Model/ImageFormat.swift index a80ab3f2..aedefd5b 100644 --- a/MindboxNotifications/Network/Model/ImageFormat.swift +++ b/MindboxNotifications/Network/Model/ImageFormat.swift @@ -7,6 +7,7 @@ // import Foundation +import MindboxLogger enum ImageFormat: String { case png, jpg, gif @@ -26,7 +27,10 @@ enum ImageFormat: String { extension ImageFormat { static func get(from data: Data) -> ImageFormat? { - guard let firstByte = data.first else { return nil } + guard let firstByte = data.first else { + Logger.common(message: "ImageFormat: Failed to get firstByte", level: .error, category: .notification) + return nil + } switch firstByte { case 0x89: return .png @@ -35,6 +39,7 @@ extension ImageFormat { case 0x47: return .gif default: + Logger.common(message: "ImageFormat: Failed to get image format", level: .error, category: .notification) return nil } } diff --git a/MindboxNotifications/Network/Model/NotificationDecoder.swift b/MindboxNotifications/Network/Model/NotificationDecoder.swift index 8f9a5872..30dbe092 100644 --- a/MindboxNotifications/Network/Model/NotificationDecoder.swift +++ b/MindboxNotifications/Network/Model/NotificationDecoder.swift @@ -8,6 +8,7 @@ import Foundation import UserNotifications +import MindboxLogger struct NotificationDecoder { var isMindboxNotification: Bool { @@ -18,15 +19,17 @@ struct NotificationDecoder { init?(request: UNNotificationRequest) { guard let userInfo = (request.content.mutableCopy() as? UNMutableNotificationContent)?.userInfo else { + Logger.common(message: "NotificationDecoder: Failed to get user info from notification content", level: .fault, category: .notification) return nil } + self.init(userInfo: userInfo) } - + init?(response: UNNotificationResponse) { self.init(request: response.notification.request) } - + init?(userInfo: [AnyHashable: Any]) { if userInfo.keys.count == 1, let innerUserInfo = userInfo["aps"] as? [AnyHashable: Any] { self.userInfo = innerUserInfo @@ -34,7 +37,7 @@ struct NotificationDecoder { self.userInfo = userInfo } } - + func decode() throws -> T { do { let data = try JSONSerialization.data(withJSONObject: userInfo, options: .prettyPrinted) @@ -43,9 +46,11 @@ struct NotificationDecoder { let payload = try decoder.decode(T.self, from: data) return payload } catch { + Logger.common(message: "NotificationDecoder: Failed to decode data into payload. error: \(error)", level: .error, category: .notification) throw error } } catch { + Logger.common(message: "NotificationDecoder: Failed to serialize userInfo into data. error: \(error)", level: .error, category: .notification) throw error } } diff --git a/MindboxNotifications/Network/Model/URLRequestBuilder.swift b/MindboxNotifications/Network/Model/URLRequestBuilder.swift index a2644e8b..91482166 100644 --- a/MindboxNotifications/Network/Model/URLRequestBuilder.swift +++ b/MindboxNotifications/Network/Model/URLRequestBuilder.swift @@ -7,6 +7,7 @@ // import Foundation +import MindboxLogger struct URLRequestBuilder { @@ -20,6 +21,7 @@ struct URLRequestBuilder { let components = makeURLComponents(for: route) guard let url = components.url else { + Logger.common(message: "URLRequestBuilder: Failed to create URL. Components: \(components)", level: .error, category: .notification) throw URLError(.badURL) } diff --git a/MindboxNotifications/Network/NetworkService.swift b/MindboxNotifications/Network/NetworkService.swift index e94f244a..56156159 100644 --- a/MindboxNotifications/Network/NetworkService.swift +++ b/MindboxNotifications/Network/NetworkService.swift @@ -32,6 +32,7 @@ class NetworkService { public func sendPushDelivered(event: Event, completion: @escaping ((Bool) -> Void)) { guard let deviceUUID = configuration.previousDeviceUUID else { completion(false) + Logger.common(message: "NetworkService: Failed to get deviceUUID. configuration.previousDeviceUUID: \(String(describing: configuration.previousDeviceUUID))", level: .error, category: .network) return } From e88eda81ad53b37dea1f7f09a043e1f1cda4ed5d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 8 Apr 2024 11:23:49 +0000 Subject: [PATCH 19/35] Update dependency fastlane to v2.220.0 --- Gemfile.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 29f25b52..d660f291 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -19,7 +19,7 @@ GEM artifactory (3.0.17) atomos (0.1.3) aws-eventstream (1.3.0) - aws-partitions (1.908.0) + aws-partitions (1.909.0) aws-sdk-core (3.191.6) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.651.0) @@ -222,7 +222,7 @@ GEM os (1.1.4) plist (3.7.1) public_suffix (4.0.7) - rake (13.2.0) + rake (13.2.1) representable (3.2.0) declarative (< 0.1.0) trailblazer-option (>= 0.1.1, < 0.2.0) From b71b4e9ee96bf3987884efdc6f13cafe3499d207 Mon Sep 17 00:00:00 2001 From: Akylbek Utekeshev Date: Fri, 29 Mar 2024 18:40:19 +0500 Subject: [PATCH 20/35] MBX-3245 Added TTL model to Config --- .../Models/Config/ConfigResponse.swift | 2 +- .../Models/Config/SettingsModel.swift | 35 +++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/Mindbox/InAppMessages/Models/Config/ConfigResponse.swift b/Mindbox/InAppMessages/Models/Config/ConfigResponse.swift index d6f39ad4..35cbcf3d 100644 --- a/Mindbox/InAppMessages/Models/Config/ConfigResponse.swift +++ b/Mindbox/InAppMessages/Models/Config/ConfigResponse.swift @@ -9,7 +9,7 @@ import Foundation import MindboxLogger struct ConfigResponse: Decodable { - let inapps: FailableDecodableArray? + var inapps: FailableDecodableArray? let monitoring: Monitoring? let settings: Settings? let abtests: [ABTest]? diff --git a/Mindbox/InAppMessages/Models/Config/SettingsModel.swift b/Mindbox/InAppMessages/Models/Config/SettingsModel.swift index 116400dc..ecf91f59 100644 --- a/Mindbox/InAppMessages/Models/Config/SettingsModel.swift +++ b/Mindbox/InAppMessages/Models/Config/SettingsModel.swift @@ -10,6 +10,7 @@ import Foundation struct Settings: Decodable, Equatable { let operations: SettingsOperations? + let ttl: TimeToLive? struct SettingsOperations: Decodable, Equatable { @@ -21,4 +22,38 @@ struct Settings: Decodable, Equatable { let systemName: String } } + + struct TimeToLive: Decodable, Equatable { + let inapps: TTLUnit + } +} + +extension Settings.TimeToLive { + struct TTLUnit: Decodable, Equatable { + let unit: Unit + let value: Int + + enum Unit: String, Decodable { + case seconds + case minutes + case hours + case days + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + value = try container.decode(Int.self, forKey: .value) + let unitString = try container.decode(String.self, forKey: .unit).lowercased() + + guard let unit = Unit(rawValue: unitString) else { + throw DecodingError.dataCorruptedError(forKey: .unit, in: container, debugDescription: "Неверная единица времени") + } + + self.unit = unit + } + + private enum CodingKeys: String, CodingKey { + case unit, value + } + } } From 8fa0d5bf3efc2c6c2ca897176e0f4d8fe8356e14 Mon Sep 17 00:00:00 2001 From: Akylbek Utekeshev Date: Tue, 2 Apr 2024 00:28:24 +0500 Subject: [PATCH 21/35] MBX-3245 Added TTL logic --- Mindbox.xcodeproj/project.pbxproj | 12 ++++ Mindbox/DI/DependencyContainer.swift | 1 + Mindbox/DI/DependencyProvider.swift | 7 +- .../InAppConfigurationManager.swift | 20 +++++- .../Services/TTLValidationService.swift | 71 +++++++++++++++++++ .../Models/Config/SettingsModel.swift | 18 ++--- Mindbox/Info.plist | 2 +- .../MBPersistenceStorage.swift | 26 +++++++ .../PersistenceStorage.swift | 2 + 9 files changed, 144 insertions(+), 15 deletions(-) create mode 100644 Mindbox/InAppMessages/Configuration/Services/TTLValidationService.swift diff --git a/Mindbox.xcodeproj/project.pbxproj b/Mindbox.xcodeproj/project.pbxproj index 8c31cecd..85415f13 100644 --- a/Mindbox.xcodeproj/project.pbxproj +++ b/Mindbox.xcodeproj/project.pbxproj @@ -318,6 +318,7 @@ F31470942B96632100E01E5C /* 16-17-TargetingRequests.json in Resources */ = {isa = PBXBuildFile; fileRef = F31470932B96632100E01E5C /* 16-17-TargetingRequests.json */; }; F31470962B96681F00E01E5C /* 27-TargetingRequests.json in Resources */ = {isa = PBXBuildFile; fileRef = F31470952B96681F00E01E5C /* 27-TargetingRequests.json */; }; F31470982B9668F100E01E5C /* 31-TargetingRequests.json in Resources */ = {isa = PBXBuildFile; fileRef = F31470972B9668F100E01E5C /* 31-TargetingRequests.json */; }; + F315503F2BBB24E20072A071 /* TTLValidationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F315503E2BBB24E20072A071 /* TTLValidationService.swift */; }; F31801F82A38649D0021774C /* InappConfigResponseValid.json in Resources */ = {isa = PBXBuildFile; fileRef = F31801F72A38649D0021774C /* InappConfigResponseValid.json */; }; F31801FB2A386A5B0021774C /* InappConfigResponseSettingsInvalid.json in Resources */ = {isa = PBXBuildFile; fileRef = F31801FA2A386A5B0021774C /* InappConfigResponseSettingsInvalid.json */; }; F31801FE2A386BE60021774C /* InappConfigResponseAbtestsInvalid.json in Resources */ = {isa = PBXBuildFile; fileRef = F31801FC2A386BE60021774C /* InappConfigResponseAbtestsInvalid.json */; }; @@ -837,6 +838,7 @@ F31470932B96632100E01E5C /* 16-17-TargetingRequests.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "16-17-TargetingRequests.json"; sourceTree = ""; }; F31470952B96681F00E01E5C /* 27-TargetingRequests.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "27-TargetingRequests.json"; sourceTree = ""; }; F31470972B9668F100E01E5C /* 31-TargetingRequests.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "31-TargetingRequests.json"; sourceTree = ""; }; + F315503E2BBB24E20072A071 /* TTLValidationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TTLValidationService.swift; sourceTree = ""; }; F31801F72A38649D0021774C /* InappConfigResponseValid.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = InappConfigResponseValid.json; sourceTree = ""; }; F31801FA2A386A5B0021774C /* InappConfigResponseSettingsInvalid.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = InappConfigResponseSettingsInvalid.json; sourceTree = ""; }; F31801FC2A386BE60021774C /* InappConfigResponseAbtestsInvalid.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = InappConfigResponseAbtestsInvalid.json; sourceTree = ""; }; @@ -1708,6 +1710,7 @@ 9B24FAAF28C74BB700F10B5D /* Configuration */ = { isa = PBXGroup; children = ( + F315503D2BBB24D60072A071 /* Services */, 9BC24E7128F6953D00C2619C /* API */, 9B4F9DF628D088A9002C9CF0 /* InAppConfig.swift */, 9B24FAB028C74BD200F10B5D /* InAppConfigurationManager.swift */, @@ -2110,6 +2113,14 @@ path = ConfigTargetings; sourceTree = ""; }; + F315503D2BBB24D60072A071 /* Services */ = { + isa = PBXGroup; + children = ( + F315503E2BBB24E20072A071 /* TTLValidationService.swift */, + ); + path = Services; + sourceTree = ""; + }; F31801F92A386A350021774C /* ConfigResponse */ = { isa = PBXGroup; children = ( @@ -3169,6 +3180,7 @@ 9B24FAB528C751E400F10B5D /* InAppImagesStorage.swift in Sources */, F3A8B9A32A3A6E6900E9C055 /* SdkVersionModel.swift in Sources */, 3333C1DE2681E9F300B60D84 /* URLRequestBuilder.swift in Sources */, + F315503F2BBB24E20072A071 /* TTLValidationService.swift in Sources */, 334F3AF5264C199900A6AC00 /* CodableDictionary.swift in Sources */, F3F5BB8A2B79F2600022AC3F /* PushNotificationFormatter.swift in Sources */, 334F3AF3264C199900A6AC00 /* AreaRequest.swift in Sources */, diff --git a/Mindbox/DI/DependencyContainer.swift b/Mindbox/DI/DependencyContainer.swift index ddd07e33..8b57805c 100644 --- a/Mindbox/DI/DependencyContainer.swift +++ b/Mindbox/DI/DependencyContainer.swift @@ -31,6 +31,7 @@ protocol DependencyContainer { var pushValidator: MindboxPushValidator { get } var inAppConfigurationDataFacade: InAppConfigurationDataFacadeProtocol { get } var userVisitManager: UserVisitManager { get } + var ttlValidationService: TTLValidationProtocol { get } } protocol InstanceFactory { diff --git a/Mindbox/DI/DependencyProvider.swift b/Mindbox/DI/DependencyProvider.swift index 0662b0b1..de696384 100644 --- a/Mindbox/DI/DependencyProvider.swift +++ b/Mindbox/DI/DependencyProvider.swift @@ -33,6 +33,7 @@ final class DependencyProvider: DependencyContainer { var pushValidator: MindboxPushValidator var inAppConfigurationDataFacade: InAppConfigurationDataFacadeProtocol var userVisitManager: UserVisitManager + var ttlValidationService: TTLValidationProtocol init() throws { utilitiesFetcher = MBUtilitiesFetcher() @@ -78,6 +79,7 @@ final class DependencyProvider: DependencyContainer { let positionFilter = ElementsPositionFilterService() let elementsFilterService = ElementsFilterService(sizeFilter: sizeFilter, positionFilter: positionFilter, colorFilter: colorFilter) let contentPositionFilterService = ContentPositionFilterService() + let variantsFilterService = VariantFilterService(layersFilter: layersFilterService, elementsFilter: elementsFilterService, contentPositionFilter: contentPositionFilterService) @@ -87,6 +89,7 @@ final class DependencyProvider: DependencyContainer { variantsFilter: variantsFilterService, sdkVersionValidator: sdkVersionValidator) + ttlValidationService = TTLValidationService(persistenceStorage: persistenceStorage) inAppConfigurationDataFacade = InAppConfigurationDataFacade(geoService: geoService, segmentationService: segmentationSevice, targetingChecker: inAppTargetingChecker, @@ -101,7 +104,9 @@ final class DependencyProvider: DependencyContainer { targetingChecker: inAppTargetingChecker, urlExtractorService: urlExtractorService, dataFacade: inAppConfigurationDataFacade), - logsManager: logsManager), + logsManager: logsManager, + persistenceStorage: persistenceStorage, + ttlValidationService: ttlValidationService), presentationManager: presentationManager, persistenceStorage: persistenceStorage ) diff --git a/Mindbox/InAppMessages/Configuration/InAppConfigurationManager.swift b/Mindbox/InAppMessages/Configuration/InAppConfigurationManager.swift index b1a585a8..cb83fe4e 100644 --- a/Mindbox/InAppMessages/Configuration/InAppConfigurationManager.swift +++ b/Mindbox/InAppMessages/Configuration/InAppConfigurationManager.swift @@ -33,17 +33,23 @@ class InAppConfigurationManager: InAppConfigurationManagerProtocol { private let inAppConfigurationMapper: InAppConfigutationMapper private let inAppConfigAPI: InAppConfigurationAPI private let logsManager: SDKLogsManagerProtocol + private let persistenceStorage: PersistenceStorage + private let ttlValidationService: TTLValidationProtocol init( inAppConfigAPI: InAppConfigurationAPI, inAppConfigRepository: InAppConfigurationRepository, inAppConfigurationMapper: InAppConfigutationMapper, - logsManager: SDKLogsManagerProtocol + logsManager: SDKLogsManagerProtocol, + persistenceStorage: PersistenceStorage, + ttlValidationService: TTLValidationProtocol ) { self.inAppConfigRepository = inAppConfigRepository self.inAppConfigurationMapper = inAppConfigurationMapper self.inAppConfigAPI = inAppConfigAPI self.logsManager = logsManager + self.persistenceStorage = persistenceStorage + self.ttlValidationService = ttlValidationService } weak var delegate: InAppConfigurationDelegate? @@ -109,9 +115,16 @@ class InAppConfigurationManager: InAppConfigurationManagerProtocol { } private func applyConfigFromCache() { - guard let cachedConfig = self.fetchConfigFromCache() else { + guard var cachedConfig = self.fetchConfigFromCache() else { + Logger.common(message: "Failed to apply configuration from cache: No cached configuration found.") return } + + if ttlValidationService.needResetInapps(config: cachedConfig) { + cachedConfig.inapps = nil + Logger.common(message: "[TTL] Resetting in-app purchases due to the expiration of the current configuration.") + } + setConfigPrepared(cachedConfig) } @@ -129,6 +142,9 @@ class InAppConfigurationManager: InAppConfigurationManagerProtocol { } private func saveConfigToCache(_ data: Data) { + let now = Date() + persistenceStorage.configDownloadDate = now + Logger.common(message: "Config download date successfully updated to: \(now).") inAppConfigRepository.saveConfigToCache(data) } diff --git a/Mindbox/InAppMessages/Configuration/Services/TTLValidationService.swift b/Mindbox/InAppMessages/Configuration/Services/TTLValidationService.swift new file mode 100644 index 00000000..a1c6684e --- /dev/null +++ b/Mindbox/InAppMessages/Configuration/Services/TTLValidationService.swift @@ -0,0 +1,71 @@ +// +// TTLValidationService.swift +// Mindbox +// +// Created by vailence on 29.03.2024. +// Copyright © 2024 Mindbox. All rights reserved. +// + +import Foundation +import MindboxLogger + +protocol TTLValidationProtocol { + func needResetInapps(config: ConfigResponse) -> Bool +} + +class TTLValidationService: TTLValidationProtocol { + + let persistenceStorage: PersistenceStorage + + init(persistenceStorage: PersistenceStorage) { + self.persistenceStorage = persistenceStorage + } + + func needResetInapps(config: ConfigResponse) -> Bool { + guard let configDownloadDate = persistenceStorage.configDownloadDate else { + Logger.common(message: "[TTL] Config download date is nil. Unable to proceed with inapps reset validation.") + return false + } + + let now = Date() + + guard let ttl = config.settings?.ttl?.inapps, + let downloadConfigDateWithTTL = getDateWithIntervalByType(ttl: ttl, date: configDownloadDate) else { + Logger.common(message: "[TTL] Variables are missing or corrupted. Inapps reset will not be performed.") + return false + } + + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss" + let nowString = dateFormatter.string(from: now) + let downloadConfigDateWithTTLString = dateFormatter.string(from: downloadConfigDateWithTTL) + + let message = """ + [TTL] Current date: \(nowString). + Config download date with TTL: \(downloadConfigDateWithTTLString). + Need to reset inapps: \(now > downloadConfigDateWithTTL). + """ + + Logger.common(message: message) + return now > downloadConfigDateWithTTL + } + + private func getDateWithIntervalByType(ttl: Settings.TimeToLive.TTLUnit, date: Date) -> Date? { + guard let type = ttl.unit, let value = ttl.value else { + Logger.common(message: "[TTL] Unable to calculate the date with TTL. The unit or value is missing.") + return nil + } + + let calendar = Calendar.current + switch type { + case .seconds: + return calendar.date(byAdding: .second, value: value, to: date) + case .minutes: + return calendar.date(byAdding: .minute, value: value, to: date) + case .hours: + return calendar.date(byAdding: .hour, value: value, to: date) + case .days: + return calendar.date(byAdding: .day, value: value, to: date) + } + } +} diff --git a/Mindbox/InAppMessages/Models/Config/SettingsModel.swift b/Mindbox/InAppMessages/Models/Config/SettingsModel.swift index ecf91f59..6212af1f 100644 --- a/Mindbox/InAppMessages/Models/Config/SettingsModel.swift +++ b/Mindbox/InAppMessages/Models/Config/SettingsModel.swift @@ -24,14 +24,14 @@ struct Settings: Decodable, Equatable { } struct TimeToLive: Decodable, Equatable { - let inapps: TTLUnit + let inapps: TTLUnit? } } extension Settings.TimeToLive { struct TTLUnit: Decodable, Equatable { - let unit: Unit - let value: Int + let unit: Unit? + let value: Int? enum Unit: String, Decodable { case seconds @@ -42,15 +42,11 @@ extension Settings.TimeToLive { init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) - value = try container.decode(Int.self, forKey: .value) - let unitString = try container.decode(String.self, forKey: .unit).lowercased() - - guard let unit = Unit(rawValue: unitString) else { - throw DecodingError.dataCorruptedError(forKey: .unit, in: container, debugDescription: "Неверная единица времени") - } - - self.unit = unit + value = try? container.decode(Int.self, forKey: .value) + let unitString = try? container.decode(String.self, forKey: .unit).lowercased() + self.unit = Unit(rawValue: unitString ?? "") } + private enum CodingKeys: String, CodingKey { case unit, value diff --git a/Mindbox/Info.plist b/Mindbox/Info.plist index 2c85542f..6e06312d 100644 --- a/Mindbox/Info.plist +++ b/Mindbox/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion - 5351 + 5297 diff --git a/Mindbox/PersistenceStorage/MBPersistenceStorage.swift b/Mindbox/PersistenceStorage/MBPersistenceStorage.swift index 3765d45f..28ecbfea 100644 --- a/Mindbox/PersistenceStorage/MBPersistenceStorage.swift +++ b/Mindbox/PersistenceStorage/MBPersistenceStorage.swift @@ -95,6 +95,23 @@ class MBPersistenceStorage: PersistenceStorage { } } + var configDownloadDate: Date? { + get { + if let dateString = configDownloadDateString { + return dateFormatter.date(from: dateString) + } else { + return nil + } + } + set { + if let date = newValue { + configDownloadDateString = dateFormatter.string(from: date) + } else { + configDownloadDateString = nil + } + } + } + var backgroundExecutions: [BackgroudExecution] { get { if let data = MBPersistenceStorage.defaults.value(forKey:"backgroundExecution") as? Data { @@ -216,6 +233,13 @@ class MBPersistenceStorage: PersistenceStorage { onDidChange?() } } + + @UserDefaultsWrapper(key: .configDownloadDate, defaultValue: nil) + private var configDownloadDateString: String? { + didSet { + onDidChange?() + } + } func reset() { installationDate = nil @@ -226,6 +250,7 @@ class MBPersistenceStorage: PersistenceStorage { deprecatedEventsRemoveDate = nil configuration = nil isNotificationsEnabled = nil + configDownloadDate = nil resetBackgroundExecutions() } @@ -268,6 +293,7 @@ extension MBPersistenceStorage { case imageLoadingMaxTimeInSeconds = "MBPersistenceStorage-imageLoadingMaxTimeInSeconds" case needUpdateInfoOnce = "MBPersistenceStorage-needUpdateInfoOnce" case userVisitCount = "MBPersistenceStorage-userVisitCount" + case configDownloadDate = "MBPersistenceStorage-configDownloadDate" } private let key: Key diff --git a/Mindbox/PersistenceStorage/PersistenceStorage.swift b/Mindbox/PersistenceStorage/PersistenceStorage.swift index afb807df..5effe32f 100644 --- a/Mindbox/PersistenceStorage/PersistenceStorage.swift +++ b/Mindbox/PersistenceStorage/PersistenceStorage.swift @@ -48,4 +48,6 @@ protocol PersistenceStorage: AnyObject { var needUpdateInfoOnce: Bool? { get set } var userVisitCount: Int? { get set } + + var configDownloadDate: Date? { get set } } From d6762bba2b2e9bcfc37cabed62a876648a704cd2 Mon Sep 17 00:00:00 2001 From: Akylbek Utekeshev Date: Mon, 8 Apr 2024 18:32:31 +0500 Subject: [PATCH 22/35] MBX-3245 Added Unit-tests --- Mindbox.xcodeproj/project.pbxproj | 4 + .../Services/TTLValidationService.swift | 2 +- .../Models/Config/SettingsModel.swift | 6 +- Mindbox/Info.plist | 2 +- MindboxTests/DI/TestDependencyProvider.swift | 2 + .../InappTTLTests.swift | 75 +++++++++++++++++++ .../Mock/MockPersistenceStorage.swift | 6 ++ 7 files changed, 94 insertions(+), 3 deletions(-) create mode 100644 MindboxTests/InApp/Tests/InAppConfigResponseTests/InappTTLTests.swift diff --git a/Mindbox.xcodeproj/project.pbxproj b/Mindbox.xcodeproj/project.pbxproj index 85415f13..d8b2e961 100644 --- a/Mindbox.xcodeproj/project.pbxproj +++ b/Mindbox.xcodeproj/project.pbxproj @@ -455,6 +455,7 @@ F3A8B9A52A3A6F2800E9C055 /* MonitoringModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3A8B9A42A3A6F2800E9C055 /* MonitoringModel.swift */; }; F3A8B9A92A3A713E00E9C055 /* SettingsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3A8B9A82A3A713E00E9C055 /* SettingsModel.swift */; }; F3A8B9AB2A3A719C00E9C055 /* ABTestModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3A8B9AA2A3A719C00E9C055 /* ABTestModel.swift */; }; + F3AF0AD02BC40FCF0063BA58 /* InappTTLTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3AF0ACF2BC40FCF0063BA58 /* InappTTLTests.swift */; }; F3B2A3982A4C79D900E2CA25 /* MindboxLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3B2A3972A4C79D900E2CA25 /* MindboxLogger.swift */; }; F3B2A39E2A4C79E200E2CA25 /* Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3B2A3992A4C79E200E2CA25 /* Status.swift */; }; F3B2A39F2A4C79E200E2CA25 /* UnknownDecodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3B2A39A2A4C79E200E2CA25 /* UnknownDecodable.swift */; }; @@ -975,6 +976,7 @@ F3A8B9A42A3A6F2800E9C055 /* MonitoringModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MonitoringModel.swift; sourceTree = ""; }; F3A8B9A82A3A713E00E9C055 /* SettingsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsModel.swift; sourceTree = ""; }; F3A8B9AA2A3A719C00E9C055 /* ABTestModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ABTestModel.swift; sourceTree = ""; }; + F3AF0ACF2BC40FCF0063BA58 /* InappTTLTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InappTTLTests.swift; sourceTree = ""; }; F3B2A3972A4C79D900E2CA25 /* MindboxLogger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MindboxLogger.swift; sourceTree = ""; }; F3B2A3992A4C79E200E2CA25 /* Status.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Status.swift; sourceTree = ""; }; F3B2A39A2A4C79E200E2CA25 /* UnknownDecodable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnknownDecodable.swift; sourceTree = ""; }; @@ -1828,6 +1830,7 @@ 9BC24E7628F6BF8C00C2619C /* InAppConfigResponseTests.swift */, A1A916E429C9191900D59D9E /* InAppConfigStub.swift */, F31470812B9634E000E01E5C /* InAppTargetingRequestsTests.swift */, + F3AF0ACF2BC40FCF0063BA58 /* InappTTLTests.swift */, ); path = InAppConfigResponseTests; sourceTree = ""; @@ -3418,6 +3421,7 @@ 9BC24E7728F6BF8C00C2619C /* InAppConfigResponseTests.swift in Sources */, F391170B2AB2E74F00852298 /* InappFilterServiceTests.swift in Sources */, F39B67AC2A3C7315005C0CCA /* MockImageDownloadService.swift in Sources */, + F3AF0AD02BC40FCF0063BA58 /* InappTTLTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Mindbox/InAppMessages/Configuration/Services/TTLValidationService.swift b/Mindbox/InAppMessages/Configuration/Services/TTLValidationService.swift index a1c6684e..a7aabef4 100644 --- a/Mindbox/InAppMessages/Configuration/Services/TTLValidationService.swift +++ b/Mindbox/InAppMessages/Configuration/Services/TTLValidationService.swift @@ -47,7 +47,7 @@ class TTLValidationService: TTLValidationProtocol { """ Logger.common(message: message) - return now > downloadConfigDateWithTTL + return nowString > downloadConfigDateWithTTLString } private func getDateWithIntervalByType(ttl: Settings.TimeToLive.TTLUnit, date: Date) -> Date? { diff --git a/Mindbox/InAppMessages/Models/Config/SettingsModel.swift b/Mindbox/InAppMessages/Models/Config/SettingsModel.swift index 6212af1f..62fa5e7d 100644 --- a/Mindbox/InAppMessages/Models/Config/SettingsModel.swift +++ b/Mindbox/InAppMessages/Models/Config/SettingsModel.swift @@ -46,7 +46,11 @@ extension Settings.TimeToLive { let unitString = try? container.decode(String.self, forKey: .unit).lowercased() self.unit = Unit(rawValue: unitString ?? "") } - + + init(unit: Unit?, value: Int?) { + self.unit = unit + self.value = value + } private enum CodingKeys: String, CodingKey { case unit, value diff --git a/Mindbox/Info.plist b/Mindbox/Info.plist index 6e06312d..9e58a101 100644 --- a/Mindbox/Info.plist +++ b/Mindbox/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion - 5297 + 5362 diff --git a/MindboxTests/DI/TestDependencyProvider.swift b/MindboxTests/DI/TestDependencyProvider.swift index fc491e7e..b1996e8e 100644 --- a/MindboxTests/DI/TestDependencyProvider.swift +++ b/MindboxTests/DI/TestDependencyProvider.swift @@ -34,6 +34,7 @@ final class TestDependencyProvider: DependencyContainer { var pushValidator: MindboxPushValidator var inAppConfigurationDataFacade: InAppConfigurationDataFacadeProtocol var userVisitManager: UserVisitManager + var ttlValidationService: TTLValidationProtocol init() throws { utilitiesFetcher = MBUtilitiesFetcher() @@ -65,6 +66,7 @@ final class TestDependencyProvider: DependencyContainer { imageDownloadService = MockImageDownloadService() abTestDeviceMixer = ABTestDeviceMixer() urlExtractorService = VariantImageUrlExtractorService() + ttlValidationService = TTLValidationService(persistenceStorage: persistenceStorage) let tracker = InAppMessagesTracker(databaseRepository: databaseRepository) inAppConfigurationDataFacade = InAppConfigurationDataFacade(geoService: geoService, segmentationService: segmentationSevice, diff --git a/MindboxTests/InApp/Tests/InAppConfigResponseTests/InappTTLTests.swift b/MindboxTests/InApp/Tests/InAppConfigResponseTests/InappTTLTests.swift new file mode 100644 index 00000000..af22facd --- /dev/null +++ b/MindboxTests/InApp/Tests/InAppConfigResponseTests/InappTTLTests.swift @@ -0,0 +1,75 @@ +// +// InappTTLTests.swift +// MindboxTests +// +// Created by vailence on 08.04.2024. +// Copyright © 2024 Mindbox. All rights reserved. +// + +import XCTest +@testable import Mindbox + +class InappTTLTests: XCTestCase { + var container: TestDependencyProvider! + var persistenceStorage: PersistenceStorage! + var service: TTLValidationProtocol! + + override func setUp() { + super.setUp() + container = try! TestDependencyProvider() + persistenceStorage = container.persistenceStorage + service = container.ttlValidationService + } + + override func tearDown() { + container = nil + persistenceStorage = nil + service = nil + super.tearDown() + } + + func testNeedResetInapps_WithTTL_Exceeds() throws { + persistenceStorage.configDownloadDate = Calendar.current.date(byAdding: .hour, value: -2, to: Date()) + let service = TTLValidationService(persistenceStorage: persistenceStorage) + let settings = Settings(operations: nil, ttl: .init(inapps: .init(unit: .hours, value: 1))) + let config = ConfigResponse(settings: settings) + let result = service.needResetInapps(config: config) + XCTAssertTrue(result, "Inapps должны быть сброшены, так как время ttl истекло.") + } + + func testNeedResetInapps_WithTTL_NotExceeded() throws { + persistenceStorage.configDownloadDate = Calendar.current.date(byAdding: .second, value: -1, to: Date()) + let service = TTLValidationService(persistenceStorage: persistenceStorage) + let settings = Settings(operations: nil, ttl: .init(inapps: .init(unit: .seconds, value: 1))) + let config = ConfigResponse(settings: settings) + let result = service.needResetInapps(config: config) + XCTAssertFalse(result, "Inapps не должны быть сброшены, так как время ttl еще не истекло.") + } + + func testNeedResetInapps_WithoutTTL() throws { + persistenceStorage.configDownloadDate = Date() + let service = TTLValidationService(persistenceStorage: persistenceStorage) + let settings = Settings(operations: nil, ttl: nil) + let config = ConfigResponse(settings: settings) + let result = service.needResetInapps(config: config) + XCTAssertFalse(result, "Inapps не должны быть сброшены, так как в конфиге отсутствует TTL.") + } + + func testNeedResetInapps_ExactlyAtTTL_ShouldNotReset() throws { + let service = TTLValidationService(persistenceStorage: persistenceStorage) + let settings = Settings(operations: nil, ttl: .init(inapps: .init(unit: .seconds, value: 1))) + let config = ConfigResponse(settings: settings) + persistenceStorage.configDownloadDate = Calendar.current.date(byAdding: .second, value: -1, to: Date()) + let result = service.needResetInapps(config: config) + XCTAssertFalse(result, "Inapps не должны сбрасываться, если текущее время точно совпадает с истечением TTL.") + } + + func testNeedResetInapps_WithTTLHalfHourAgo_NotExceeded() throws { + persistenceStorage.configDownloadDate = Calendar.current.date(byAdding: .minute, value: -30, to: Date()) + let service = TTLValidationService(persistenceStorage: persistenceStorage) + let settings = Settings(operations: nil, ttl: .init(inapps: .init(unit: .hours, value: 1))) + let config = ConfigResponse(settings: settings) + let result = service.needResetInapps(config: config) + XCTAssertFalse(result, "Inapps не должны быть сброшены, так как время TTL еще не истекло.") + } +} diff --git a/MindboxTests/Mock/MockPersistenceStorage.swift b/MindboxTests/Mock/MockPersistenceStorage.swift index 2ffabf2e..1488d223 100644 --- a/MindboxTests/Mock/MockPersistenceStorage.swift +++ b/MindboxTests/Mock/MockPersistenceStorage.swift @@ -88,6 +88,12 @@ class MockPersistenceStorage: PersistenceStorage { set { _userVisitCount = newValue } } + var configDownloadDate: Date? { + didSet { + onDidChange?() + } + } + func reset() { installationDate = nil deviceUUID = nil From d13d11697fc96eaeb1e741f805a6aa34667817ed Mon Sep 17 00:00:00 2001 From: Akylbek Utekeshev Date: Tue, 9 Apr 2024 00:28:32 +0500 Subject: [PATCH 23/35] MBX-3245 Check without miliseconds --- .../Services/TTLValidationService.swift | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/Mindbox/InAppMessages/Configuration/Services/TTLValidationService.swift b/Mindbox/InAppMessages/Configuration/Services/TTLValidationService.swift index a7aabef4..a1ac3d64 100644 --- a/Mindbox/InAppMessages/Configuration/Services/TTLValidationService.swift +++ b/Mindbox/InAppMessages/Configuration/Services/TTLValidationService.swift @@ -35,19 +35,29 @@ class TTLValidationService: TTLValidationProtocol { return false } + let calendar = Calendar.current + let nowComponents = calendar.dateComponents([.year, .month, .day, .hour, .minute, .second], from: now) + let ttlComponents = calendar.dateComponents([.year, .month, .day, .hour, .minute, .second], from: downloadConfigDateWithTTL) + + guard let nowWithoutMilliseconds = calendar.date(from: nowComponents), + let downloadConfigDateWithTTLWithoutMilliseconds = calendar.date(from: ttlComponents) else { + Logger.common(message: "[TTL] Error in date components. Inapps reset will not be performed.") + return false + } + let dateFormatter = DateFormatter() dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss" - let nowString = dateFormatter.string(from: now) - let downloadConfigDateWithTTLString = dateFormatter.string(from: downloadConfigDateWithTTL) + let nowString = dateFormatter.string(from: nowWithoutMilliseconds) + let downloadConfigDateWithTTLString = dateFormatter.string(from: downloadConfigDateWithTTLWithoutMilliseconds) let message = """ [TTL] Current date: \(nowString). Config download date with TTL: \(downloadConfigDateWithTTLString). - Need to reset inapps: \(now > downloadConfigDateWithTTL). + Need to reset inapps: \(nowWithoutMilliseconds > downloadConfigDateWithTTLWithoutMilliseconds). """ Logger.common(message: message) - return nowString > downloadConfigDateWithTTLString + return nowWithoutMilliseconds > downloadConfigDateWithTTLWithoutMilliseconds } private func getDateWithIntervalByType(ttl: Settings.TimeToLive.TTLUnit, date: Date) -> Date? { From e36024a36b0ffbcecee2a1ed780f5581e9134e82 Mon Sep 17 00:00:00 2001 From: Akylbek Utekeshev Date: Wed, 10 Apr 2024 14:38:07 +0500 Subject: [PATCH 24/35] MBX-3245 TTL build trigger --- .../Configuration/Services/TTLValidationService.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mindbox/InAppMessages/Configuration/Services/TTLValidationService.swift b/Mindbox/InAppMessages/Configuration/Services/TTLValidationService.swift index a1ac3d64..7249d169 100644 --- a/Mindbox/InAppMessages/Configuration/Services/TTLValidationService.swift +++ b/Mindbox/InAppMessages/Configuration/Services/TTLValidationService.swift @@ -46,7 +46,7 @@ class TTLValidationService: TTLValidationProtocol { } let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss" + dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss" let nowString = dateFormatter.string(from: nowWithoutMilliseconds) let downloadConfigDateWithTTLString = dateFormatter.string(from: downloadConfigDateWithTTLWithoutMilliseconds) From d4c04303262ec175c6f9f342870bd873e5b81601 Mon Sep 17 00:00:00 2001 From: Dmitry Erofeev <56806136+kniksees@users.noreply.github.com> Date: Fri, 12 Apr 2024 12:06:16 +0300 Subject: [PATCH 25/35] MBX-3296-fixPayloadCopying (#341) * bug/MBX-3296-fixPayloadCopying: add payload copying * bug/MBX-3296-fixPayloadCopying: fix payload coping --- .../PresentationActionUseCase/PushPermissionActionUseCase.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mindbox/InAppMessages/Presentation/Manager/UseCases/PresentationActionUseCase/PushPermissionActionUseCase.swift b/Mindbox/InAppMessages/Presentation/Manager/UseCases/PresentationActionUseCase/PushPermissionActionUseCase.swift index fe48a3aa..d0bcb12a 100644 --- a/Mindbox/InAppMessages/Presentation/Manager/UseCases/PresentationActionUseCase/PushPermissionActionUseCase.swift +++ b/Mindbox/InAppMessages/Presentation/Manager/UseCases/PresentationActionUseCase/PushPermissionActionUseCase.swift @@ -35,13 +35,13 @@ final class PushPermissionActionUseCase: PresentationActionUseCaseProtocol { func requestOrOpenSettingsForNotifications() { UNUserNotificationCenter.current().getNotificationSettings { settings in Logger.common(message: "Status of notification permission: \(settings.authorizationStatus.description)", level: .debug, category: .inAppMessages) + UIPasteboard.general.string = self.model.intentPayload switch settings.authorizationStatus { case .notDetermined: self.pushNotificationRequest() case .denied: self.openPushNotificationSettings() case .authorized, .provisional, .ephemeral: - UIPasteboard.general.string = self.model.intentPayload return @unknown default: Logger.common(message: "Encountered an unknown notification authorization status: \(settings.authorizationStatus.description)", level: .debug, category: .inAppMessages) From 5ab228761bd34ee4836ff8982b2ec25acc6da9d0 Mon Sep 17 00:00:00 2001 From: Dmitry Erofeev <56806136+kniksees@users.noreply.github.com> Date: Mon, 15 Apr 2024 21:59:01 +0300 Subject: [PATCH 26/35] bug/MBX-3307-fixInfoShowingInExample: fix info showing in example (#342) --- Example/Example/AppDelegate.swift | 3 --- 1 file changed, 3 deletions(-) diff --git a/Example/Example/AppDelegate.swift b/Example/Example/AppDelegate.swift index 79b2aec6..b08cc4e7 100644 --- a/Example/Example/AppDelegate.swift +++ b/Example/Example/AppDelegate.swift @@ -27,9 +27,6 @@ class AppDelegate: MindboxAppDelegate { shouldCreateCustomer: true ) Mindbox.shared.initialization(configuration: mindboxSdkConfig) - Mindbox.shared.getDeviceUUID { deviceUUID in - print(deviceUUID) - } } catch { print(error) } From ab49ccd1a5c87a358a9947ab7d27fd87a3d802f1 Mon Sep 17 00:00:00 2001 From: Dmitry Erofeev <56806136+kniksees@users.noreply.github.com> Date: Tue, 16 Apr 2024 15:01:04 +0300 Subject: [PATCH 27/35] MBX-3291: Add gif images to In-Apps (#339) * feature/MBX-3291-GifImages: add .gif with animation to In-Apps * feature/MBX-3291-GifImages: add .gif with animation to rich-push * feature/MBX-3291-GifImages: update project.pbxproj * feature/MBX-3291-GifImages: update ImageFormat * feature/MBX-3291-GifImages: update ImageFormat * Revert "feature/MBX-3291-GifImages: update ImageFormat" This reverts commit 4a49e80bc3a0dddbb3a86b68cbb35fbf564e6254. * Revert "feature/MBX-3291-GifImages: update ImageFormat" This reverts commit 40cd36ed2af7f4bd8045600515b1794be57a621b. * Revert "feature/MBX-3291-GifImages: add .gif with animation to rich-push" This reverts commit db4b34eaccf03566621fa2ad8c3cc8bbe62e3d01. --- Mindbox.xcodeproj/project.pbxproj | 4 + .../InAppMessages/Images/ImageFormat.swift | 79 +++++++++++++++++++ .../Services/ImageDownloadService.swift | 2 +- Mindbox/Info.plist | 2 +- 4 files changed, 85 insertions(+), 2 deletions(-) create mode 100644 Mindbox/InAppMessages/Images/ImageFormat.swift diff --git a/Mindbox.xcodeproj/project.pbxproj b/Mindbox.xcodeproj/project.pbxproj index d8b2e961..05c0ee8c 100644 --- a/Mindbox.xcodeproj/project.pbxproj +++ b/Mindbox.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 0A3D045A2BC6803E00E1FC52 /* ImageFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A3D04592BC6803E00E1FC52 /* ImageFormat.swift */; }; 3132DFF625C2A811007FE358 /* TestDependencyProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3132DFF525C2A811007FE358 /* TestDependencyProvider.swift */; }; 313B233A25ADEA0F00A1CB72 /* Mindbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 313B233025ADEA0F00A1CB72 /* Mindbox.framework */; }; 313B233F25ADEA0F00A1CB72 /* MindboxTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 313B233E25ADEA0F00A1CB72 /* MindboxTests.swift */; }; @@ -522,6 +523,7 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 0A3D04592BC6803E00E1FC52 /* ImageFormat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageFormat.swift; sourceTree = ""; }; 3132DFF525C2A811007FE358 /* TestDependencyProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestDependencyProvider.swift; sourceTree = ""; }; 313B233025ADEA0F00A1CB72 /* Mindbox.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Mindbox.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 313B233325ADEA0F00A1CB72 /* Mindbox.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Mindbox.h; sourceTree = ""; }; @@ -1725,6 +1727,7 @@ isa = PBXGroup; children = ( 9B24FAB428C751E400F10B5D /* InAppImagesStorage.swift */, + 0A3D04592BC6803E00E1FC52 /* ImageFormat.swift */, ); path = Images; sourceTree = ""; @@ -3208,6 +3211,7 @@ 9BC24E7528F6953D00C2619C /* ConfigResponse.swift in Sources */, F331DD332A84D57800222120 /* VariantImageUrlExtractorService.swift in Sources */, 33072F482664CCBA001F1AB2 /* PoolResponse.swift in Sources */, + 0A3D045A2BC6803E00E1FC52 /* ImageFormat.swift in Sources */, 337C79562656533F0092B580 /* ViewProductRequest.swift in Sources */, 6FDD145F266F7CD900A50C35 /* DiscountResponse.swift in Sources */, 33072F462664CC9A001F1AB2 /* UsedPointOfContactResponse.swift in Sources */, diff --git a/Mindbox/InAppMessages/Images/ImageFormat.swift b/Mindbox/InAppMessages/Images/ImageFormat.swift new file mode 100644 index 00000000..5510dc00 --- /dev/null +++ b/Mindbox/InAppMessages/Images/ImageFormat.swift @@ -0,0 +1,79 @@ +// +// ImageFormat.swift +// Mindbox +// +// Created by Дмитрий Ерофеев on 09.04.2024. +// + +import Foundation +import ImageIO +import UIKit + +enum ImageFormat: String { + case png, jpg, gif + + init?(_ data: Data) { + if let type = ImageFormat.get(from: data) { + self = type + } else { + return nil + } + } +} + +extension ImageFormat { + + private static func get(from data: Data?) -> ImageFormat? { + + guard let firstByte = data?.first else { return nil } + + switch firstByte { + case 0x89: + return .png + case 0xFF: + return .jpg + case 0x47: + return .gif + default: + return nil + } + } + + private static func animatedImage(withGIFData data: Data) -> UIImage? { + + guard let source = CGImageSourceCreateWithData(data as CFData, nil) else { return nil } + + let frameCount = CGImageSourceGetCount(source) + var frames: [UIImage] = [] + var gifDuration = 0.0 + + for i in 0.. UIImage? { + + guard let imageData else { return nil } + let imageFormat = ImageFormat.get(from: imageData) + + switch imageFormat { + case .gif: + return animatedImage(withGIFData: imageData) + default: + return UIImage(data: imageData) + } + } +} diff --git a/Mindbox/InAppMessages/InAppConfigurationMapper/Services/ImageDownloadService.swift b/Mindbox/InAppMessages/InAppConfigurationMapper/Services/ImageDownloadService.swift index 28ffce50..d39d4fe1 100644 --- a/Mindbox/InAppMessages/InAppConfigurationMapper/Services/ImageDownloadService.swift +++ b/Mindbox/InAppMessages/InAppConfigurationMapper/Services/ImageDownloadService.swift @@ -37,7 +37,7 @@ class ImageDownloadService: ImageDownloadServiceProtocol { } else if let localURL = localURL { do { let imageData = try Data(contentsOf: localURL) - guard let image = UIImage(data: imageData) else { + guard let image = ImageFormat.getImage(imageData: imageData) else { Logger.common(message: "Inapps image is incorrect. [URL]: \(localURL)", level: .debug, category: .inAppMessages) let error = NSError(domain: "", code: NSURLErrorCannotDecodeContentData, userInfo: nil) completion(.failure(error)) diff --git a/Mindbox/Info.plist b/Mindbox/Info.plist index 9e58a101..181b92ca 100644 --- a/Mindbox/Info.plist +++ b/Mindbox/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion - 5362 + 5364 From 4ee6b7d3ebcca4ce22cefc51d412689a8efe91db Mon Sep 17 00:00:00 2001 From: Dmitry Erofeev <56806136+kniksees@users.noreply.github.com> Date: Tue, 16 Apr 2024 17:28:28 +0300 Subject: [PATCH 28/35] MBX-3296: fixPayloadCopying (#343) * bug/MBX-3296-fixPayloadCopying: add payload copying * bug/MBX-3296-fixPayloadCopying: fix payload coping * bug/MBX-3296-fixPayloadCopying: update PushPermissionActionUseCase * bug/MBX-3296-fixPayloadCopying: update PushPermissionActionUseCase * bug/MBX-3296-fixPayloadCopying: remove print * bug/MBX-3296-fixPayloadCopying: update PushPermissionActionUseCase * bug/MBX-3296-fixPayloadCopying: remove print --- .../PushPermissionActionUseCase.swift | 34 ++++++++----------- Mindbox/Info.plist | 2 +- 2 files changed, 16 insertions(+), 20 deletions(-) diff --git a/Mindbox/InAppMessages/Presentation/Manager/UseCases/PresentationActionUseCase/PushPermissionActionUseCase.swift b/Mindbox/InAppMessages/Presentation/Manager/UseCases/PresentationActionUseCase/PushPermissionActionUseCase.swift index d0bcb12a..71c1a96c 100644 --- a/Mindbox/InAppMessages/Presentation/Manager/UseCases/PresentationActionUseCase/PushPermissionActionUseCase.swift +++ b/Mindbox/InAppMessages/Presentation/Manager/UseCases/PresentationActionUseCase/PushPermissionActionUseCase.swift @@ -28,24 +28,28 @@ final class PushPermissionActionUseCase: PresentationActionUseCaseProtocol { ) { tracker.trackClick(id: id) Logger.common(message: "In-app with push permission | ID: \(id)", level: .debug, category: .inAppMessages) - requestOrOpenSettingsForNotifications() + requestOrOpenSettingsForNotifications { settingsUrl in + onTap(settingsUrl, self.model.intentPayload) + } close() } - func requestOrOpenSettingsForNotifications() { + func requestOrOpenSettingsForNotifications(_ completion: @escaping (URL?) -> Void) { UNUserNotificationCenter.current().getNotificationSettings { settings in Logger.common(message: "Status of notification permission: \(settings.authorizationStatus.description)", level: .debug, category: .inAppMessages) - UIPasteboard.general.string = self.model.intentPayload switch settings.authorizationStatus { case .notDetermined: + completion(nil) self.pushNotificationRequest() case .denied: - self.openPushNotificationSettings() + self.getPushNotificationSettingsUrl { url in + completion(url) + } case .authorized, .provisional, .ephemeral: - return + completion(nil) @unknown default: + completion(nil) Logger.common(message: "Encountered an unknown notification authorization status: \(settings.authorizationStatus.description)", level: .debug, category: .inAppMessages) - return } } } @@ -68,7 +72,7 @@ final class PushPermissionActionUseCase: PresentationActionUseCaseProtocol { } } - private func openPushNotificationSettings() { + private func getPushNotificationSettingsUrl(_ completion: @escaping (URL?) -> Void) { DispatchQueue.main.async { let settingsUrl: URL? if #available(iOS 16.0, *) { @@ -77,19 +81,11 @@ final class PushPermissionActionUseCase: PresentationActionUseCaseProtocol { settingsUrl = URL(string: UIApplication.openSettingsURLString) } - guard let settingsUrl = settingsUrl, UIApplication.shared.canOpenURL(settingsUrl) else { + if let settingsUrl, UIApplication.shared.canOpenURL(settingsUrl) { + completion(settingsUrl) + } else { Logger.common(message: "Failed to parse the settings URL or encountered an issue opening it.", level: .debug, category: .inAppMessages) - return - } - - if UIApplication.shared.canOpenURL(settingsUrl) { - if #available(iOS 10.0, *) { - UIApplication.shared.open(settingsUrl) - } else { - UIApplication.shared.openURL(settingsUrl) - } - - Logger.common(message: "Navigated to app settings for notification permission.", level: .debug, category: .inAppMessages) + completion(nil) } } } diff --git a/Mindbox/Info.plist b/Mindbox/Info.plist index 181b92ca..000e491a 100644 --- a/Mindbox/Info.plist +++ b/Mindbox/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion - 5364 + 5365 From 89376ed8e3fc0273d9e99ab92b672f21837dd353 Mon Sep 17 00:00:00 2001 From: Dmitry Erofeev <56806136+kniksees@users.noreply.github.com> Date: Wed, 17 Apr 2024 13:34:34 +0300 Subject: [PATCH 29/35] Revert "MBX-3296: fixPayloadCopying (#343)" (#348) This reverts commit 4ee6b7d3ebcca4ce22cefc51d412689a8efe91db. --- .../PushPermissionActionUseCase.swift | 34 +++++++++++-------- Mindbox/Info.plist | 2 +- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/Mindbox/InAppMessages/Presentation/Manager/UseCases/PresentationActionUseCase/PushPermissionActionUseCase.swift b/Mindbox/InAppMessages/Presentation/Manager/UseCases/PresentationActionUseCase/PushPermissionActionUseCase.swift index 71c1a96c..d0bcb12a 100644 --- a/Mindbox/InAppMessages/Presentation/Manager/UseCases/PresentationActionUseCase/PushPermissionActionUseCase.swift +++ b/Mindbox/InAppMessages/Presentation/Manager/UseCases/PresentationActionUseCase/PushPermissionActionUseCase.swift @@ -28,28 +28,24 @@ final class PushPermissionActionUseCase: PresentationActionUseCaseProtocol { ) { tracker.trackClick(id: id) Logger.common(message: "In-app with push permission | ID: \(id)", level: .debug, category: .inAppMessages) - requestOrOpenSettingsForNotifications { settingsUrl in - onTap(settingsUrl, self.model.intentPayload) - } + requestOrOpenSettingsForNotifications() close() } - func requestOrOpenSettingsForNotifications(_ completion: @escaping (URL?) -> Void) { + func requestOrOpenSettingsForNotifications() { UNUserNotificationCenter.current().getNotificationSettings { settings in Logger.common(message: "Status of notification permission: \(settings.authorizationStatus.description)", level: .debug, category: .inAppMessages) + UIPasteboard.general.string = self.model.intentPayload switch settings.authorizationStatus { case .notDetermined: - completion(nil) self.pushNotificationRequest() case .denied: - self.getPushNotificationSettingsUrl { url in - completion(url) - } + self.openPushNotificationSettings() case .authorized, .provisional, .ephemeral: - completion(nil) + return @unknown default: - completion(nil) Logger.common(message: "Encountered an unknown notification authorization status: \(settings.authorizationStatus.description)", level: .debug, category: .inAppMessages) + return } } } @@ -72,7 +68,7 @@ final class PushPermissionActionUseCase: PresentationActionUseCaseProtocol { } } - private func getPushNotificationSettingsUrl(_ completion: @escaping (URL?) -> Void) { + private func openPushNotificationSettings() { DispatchQueue.main.async { let settingsUrl: URL? if #available(iOS 16.0, *) { @@ -81,11 +77,19 @@ final class PushPermissionActionUseCase: PresentationActionUseCaseProtocol { settingsUrl = URL(string: UIApplication.openSettingsURLString) } - if let settingsUrl, UIApplication.shared.canOpenURL(settingsUrl) { - completion(settingsUrl) - } else { + guard let settingsUrl = settingsUrl, UIApplication.shared.canOpenURL(settingsUrl) else { Logger.common(message: "Failed to parse the settings URL or encountered an issue opening it.", level: .debug, category: .inAppMessages) - completion(nil) + return + } + + if UIApplication.shared.canOpenURL(settingsUrl) { + if #available(iOS 10.0, *) { + UIApplication.shared.open(settingsUrl) + } else { + UIApplication.shared.openURL(settingsUrl) + } + + Logger.common(message: "Navigated to app settings for notification permission.", level: .debug, category: .inAppMessages) } } } diff --git a/Mindbox/Info.plist b/Mindbox/Info.plist index 000e491a..181b92ca 100644 --- a/Mindbox/Info.plist +++ b/Mindbox/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion - 5365 + 5364 From 741693cfa5bbaf1bf5a09b796644d7975be9ef39 Mon Sep 17 00:00:00 2001 From: Dmitry Erofeev <56806136+kniksees@users.noreply.github.com> Date: Wed, 17 Apr 2024 13:36:58 +0300 Subject: [PATCH 30/35] bug/MBX-3296-fixPayloadCopying: fixPayloadCopying (#347) --- .../PresentationActionUseCase/PushPermissionActionUseCase.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mindbox/InAppMessages/Presentation/Manager/UseCases/PresentationActionUseCase/PushPermissionActionUseCase.swift b/Mindbox/InAppMessages/Presentation/Manager/UseCases/PresentationActionUseCase/PushPermissionActionUseCase.swift index d0bcb12a..f2a98d9c 100644 --- a/Mindbox/InAppMessages/Presentation/Manager/UseCases/PresentationActionUseCase/PushPermissionActionUseCase.swift +++ b/Mindbox/InAppMessages/Presentation/Manager/UseCases/PresentationActionUseCase/PushPermissionActionUseCase.swift @@ -29,13 +29,13 @@ final class PushPermissionActionUseCase: PresentationActionUseCaseProtocol { tracker.trackClick(id: id) Logger.common(message: "In-app with push permission | ID: \(id)", level: .debug, category: .inAppMessages) requestOrOpenSettingsForNotifications() + onTap(nil, model.intentPayload) close() } func requestOrOpenSettingsForNotifications() { UNUserNotificationCenter.current().getNotificationSettings { settings in Logger.common(message: "Status of notification permission: \(settings.authorizationStatus.description)", level: .debug, category: .inAppMessages) - UIPasteboard.general.string = self.model.intentPayload switch settings.authorizationStatus { case .notDetermined: self.pushNotificationRequest() From adce3d6834900b2957bee582b88726475b3f94fe Mon Sep 17 00:00:00 2001 From: Akylbek Utekeshev Date: Wed, 17 Apr 2024 22:36:36 +0500 Subject: [PATCH 31/35] MBX-3245 Added additional logs --- .../InAppConfigurationManager.swift | 4 ++-- .../Services/TTLValidationService.swift | 11 +++-------- Mindbox/Model/Common/MBDate.swift | 17 +++++++++++++++++ 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/Mindbox/InAppMessages/Configuration/InAppConfigurationManager.swift b/Mindbox/InAppMessages/Configuration/InAppConfigurationManager.swift index cb83fe4e..c898ae6d 100644 --- a/Mindbox/InAppMessages/Configuration/InAppConfigurationManager.swift +++ b/Mindbox/InAppMessages/Configuration/InAppConfigurationManager.swift @@ -122,7 +122,7 @@ class InAppConfigurationManager: InAppConfigurationManagerProtocol { if ttlValidationService.needResetInapps(config: cachedConfig) { cachedConfig.inapps = nil - Logger.common(message: "[TTL] Resetting in-app purchases due to the expiration of the current configuration.") + Logger.common(message: "[TTL] Resetting in-app due to the expiration of the current configuration.") } setConfigPrepared(cachedConfig) @@ -144,7 +144,7 @@ class InAppConfigurationManager: InAppConfigurationManagerProtocol { private func saveConfigToCache(_ data: Data) { let now = Date() persistenceStorage.configDownloadDate = now - Logger.common(message: "Config download date successfully updated to: \(now).") + Logger.common(message: "[TTL] Config download date successfully updated to: \(now.asDateTimeWithSeconds).") inAppConfigRepository.saveConfigToCache(data) } diff --git a/Mindbox/InAppMessages/Configuration/Services/TTLValidationService.swift b/Mindbox/InAppMessages/Configuration/Services/TTLValidationService.swift index 7249d169..a7e394bb 100644 --- a/Mindbox/InAppMessages/Configuration/Services/TTLValidationService.swift +++ b/Mindbox/InAppMessages/Configuration/Services/TTLValidationService.swift @@ -44,15 +44,10 @@ class TTLValidationService: TTLValidationProtocol { Logger.common(message: "[TTL] Error in date components. Inapps reset will not be performed.") return false } - - let dateFormatter = DateFormatter() - dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss" - let nowString = dateFormatter.string(from: nowWithoutMilliseconds) - let downloadConfigDateWithTTLString = dateFormatter.string(from: downloadConfigDateWithTTLWithoutMilliseconds) - + let message = """ - [TTL] Current date: \(nowString). - Config download date with TTL: \(downloadConfigDateWithTTLString). + [TTL] Current date: \(nowWithoutMilliseconds.asDateTimeWithSeconds). + Config with TTL valid until: \(downloadConfigDateWithTTLWithoutMilliseconds.asDateTimeWithSeconds). Need to reset inapps: \(nowWithoutMilliseconds > downloadConfigDateWithTTLWithoutMilliseconds). """ diff --git a/Mindbox/Model/Common/MBDate.swift b/Mindbox/Model/Common/MBDate.swift index 754fca7b..ec622458 100644 --- a/Mindbox/Model/Common/MBDate.swift +++ b/Mindbox/Model/Common/MBDate.swift @@ -79,6 +79,10 @@ extension Date { public var asDateTime: DateTime { return DateTime(self) } + + public var asDateTimeWithSeconds: String { + return DateTimeWithSeconds(self).string + } } fileprivate extension Date { @@ -108,3 +112,16 @@ fileprivate extension String { return Date.Formatter.iso8601.date(from: data) } } + +public final class DateTimeWithSeconds: MBDate { + override var dateFormat: String { + "yyyy-MM-dd HH:mm:ss" + } + + override func decodeWithFormat(_ rawString: String) -> Date? { + let formatter = DateFormatter() + formatter.dateFormat = dateFormat + formatter.timeZone = TimeZone(secondsFromGMT: 0) + return formatter.date(from: rawString) + } +} From e40299c3888edf1fbbb20d0b03edb2d78d9ebd08 Mon Sep 17 00:00:00 2001 From: Akylbek Utekeshev Date: Wed, 17 Apr 2024 22:36:50 +0500 Subject: [PATCH 32/35] MBX-3245 Remove cachePolicy --- .../InAppMessages/Configuration/API/InAppConfigurationAPI.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mindbox/InAppMessages/Configuration/API/InAppConfigurationAPI.swift b/Mindbox/InAppMessages/Configuration/API/InAppConfigurationAPI.swift index e70fefb8..fa58e556 100644 --- a/Mindbox/InAppMessages/Configuration/API/InAppConfigurationAPI.swift +++ b/Mindbox/InAppMessages/Configuration/API/InAppConfigurationAPI.swift @@ -36,7 +36,7 @@ class InAppConfigurationAPI { let builder = URLRequestBuilder(domain: configuration.domain) var urlRequest = try builder.asURLRequest(route: route) Logger.network(request: urlRequest) - urlRequest.cachePolicy = .useProtocolCachePolicy + urlRequest.cachePolicy = .reloadIgnoringLocalAndRemoteCacheData URLSession.shared.dataTask(with: urlRequest) { [self] data, response, error in completionQueue.async { let result = self.completeDownloadTask(data, response: response, error: error) From 4fc380323a4e647b22ba66c89033c6679b6db5f3 Mon Sep 17 00:00:00 2001 From: Akylbek Utekeshev Date: Thu, 18 Apr 2024 12:59:23 +0500 Subject: [PATCH 33/35] Revert "Merge pull request #337 from mindbox-cloud/feature/MBX-3245-TTL" This reverts commit 6ab021278620b7337fc025e6d9addec8c9ee596b, reversing changes made to 53da899f1e5c3913e8b70e626b5b5debd3cdbe72. --- Mindbox.xcodeproj/project.pbxproj | 16 ---- Mindbox/DI/DependencyContainer.swift | 1 - Mindbox/DI/DependencyProvider.swift | 7 +- .../InAppConfigurationManager.swift | 20 +---- .../Services/TTLValidationService.swift | 76 ------------------- .../Models/Config/ConfigResponse.swift | 2 +- .../Models/Config/SettingsModel.swift | 35 --------- Mindbox/Info.plist | 2 +- .../MBPersistenceStorage.swift | 26 ------- .../PersistenceStorage.swift | 2 - MindboxTests/DI/TestDependencyProvider.swift | 2 - .../InappTTLTests.swift | 75 ------------------ .../Mock/MockPersistenceStorage.swift | 6 -- 13 files changed, 5 insertions(+), 265 deletions(-) delete mode 100644 Mindbox/InAppMessages/Configuration/Services/TTLValidationService.swift delete mode 100644 MindboxTests/InApp/Tests/InAppConfigResponseTests/InappTTLTests.swift diff --git a/Mindbox.xcodeproj/project.pbxproj b/Mindbox.xcodeproj/project.pbxproj index 05c0ee8c..1e9de79c 100644 --- a/Mindbox.xcodeproj/project.pbxproj +++ b/Mindbox.xcodeproj/project.pbxproj @@ -319,7 +319,6 @@ F31470942B96632100E01E5C /* 16-17-TargetingRequests.json in Resources */ = {isa = PBXBuildFile; fileRef = F31470932B96632100E01E5C /* 16-17-TargetingRequests.json */; }; F31470962B96681F00E01E5C /* 27-TargetingRequests.json in Resources */ = {isa = PBXBuildFile; fileRef = F31470952B96681F00E01E5C /* 27-TargetingRequests.json */; }; F31470982B9668F100E01E5C /* 31-TargetingRequests.json in Resources */ = {isa = PBXBuildFile; fileRef = F31470972B9668F100E01E5C /* 31-TargetingRequests.json */; }; - F315503F2BBB24E20072A071 /* TTLValidationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F315503E2BBB24E20072A071 /* TTLValidationService.swift */; }; F31801F82A38649D0021774C /* InappConfigResponseValid.json in Resources */ = {isa = PBXBuildFile; fileRef = F31801F72A38649D0021774C /* InappConfigResponseValid.json */; }; F31801FB2A386A5B0021774C /* InappConfigResponseSettingsInvalid.json in Resources */ = {isa = PBXBuildFile; fileRef = F31801FA2A386A5B0021774C /* InappConfigResponseSettingsInvalid.json */; }; F31801FE2A386BE60021774C /* InappConfigResponseAbtestsInvalid.json in Resources */ = {isa = PBXBuildFile; fileRef = F31801FC2A386BE60021774C /* InappConfigResponseAbtestsInvalid.json */; }; @@ -456,7 +455,6 @@ F3A8B9A52A3A6F2800E9C055 /* MonitoringModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3A8B9A42A3A6F2800E9C055 /* MonitoringModel.swift */; }; F3A8B9A92A3A713E00E9C055 /* SettingsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3A8B9A82A3A713E00E9C055 /* SettingsModel.swift */; }; F3A8B9AB2A3A719C00E9C055 /* ABTestModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3A8B9AA2A3A719C00E9C055 /* ABTestModel.swift */; }; - F3AF0AD02BC40FCF0063BA58 /* InappTTLTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3AF0ACF2BC40FCF0063BA58 /* InappTTLTests.swift */; }; F3B2A3982A4C79D900E2CA25 /* MindboxLogger.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3B2A3972A4C79D900E2CA25 /* MindboxLogger.swift */; }; F3B2A39E2A4C79E200E2CA25 /* Status.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3B2A3992A4C79E200E2CA25 /* Status.swift */; }; F3B2A39F2A4C79E200E2CA25 /* UnknownDecodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = F3B2A39A2A4C79E200E2CA25 /* UnknownDecodable.swift */; }; @@ -841,7 +839,6 @@ F31470932B96632100E01E5C /* 16-17-TargetingRequests.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "16-17-TargetingRequests.json"; sourceTree = ""; }; F31470952B96681F00E01E5C /* 27-TargetingRequests.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "27-TargetingRequests.json"; sourceTree = ""; }; F31470972B9668F100E01E5C /* 31-TargetingRequests.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "31-TargetingRequests.json"; sourceTree = ""; }; - F315503E2BBB24E20072A071 /* TTLValidationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TTLValidationService.swift; sourceTree = ""; }; F31801F72A38649D0021774C /* InappConfigResponseValid.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = InappConfigResponseValid.json; sourceTree = ""; }; F31801FA2A386A5B0021774C /* InappConfigResponseSettingsInvalid.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = InappConfigResponseSettingsInvalid.json; sourceTree = ""; }; F31801FC2A386BE60021774C /* InappConfigResponseAbtestsInvalid.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = InappConfigResponseAbtestsInvalid.json; sourceTree = ""; }; @@ -978,7 +975,6 @@ F3A8B9A42A3A6F2800E9C055 /* MonitoringModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MonitoringModel.swift; sourceTree = ""; }; F3A8B9A82A3A713E00E9C055 /* SettingsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsModel.swift; sourceTree = ""; }; F3A8B9AA2A3A719C00E9C055 /* ABTestModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ABTestModel.swift; sourceTree = ""; }; - F3AF0ACF2BC40FCF0063BA58 /* InappTTLTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InappTTLTests.swift; sourceTree = ""; }; F3B2A3972A4C79D900E2CA25 /* MindboxLogger.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MindboxLogger.swift; sourceTree = ""; }; F3B2A3992A4C79E200E2CA25 /* Status.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Status.swift; sourceTree = ""; }; F3B2A39A2A4C79E200E2CA25 /* UnknownDecodable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnknownDecodable.swift; sourceTree = ""; }; @@ -1714,7 +1710,6 @@ 9B24FAAF28C74BB700F10B5D /* Configuration */ = { isa = PBXGroup; children = ( - F315503D2BBB24D60072A071 /* Services */, 9BC24E7128F6953D00C2619C /* API */, 9B4F9DF628D088A9002C9CF0 /* InAppConfig.swift */, 9B24FAB028C74BD200F10B5D /* InAppConfigurationManager.swift */, @@ -1833,7 +1828,6 @@ 9BC24E7628F6BF8C00C2619C /* InAppConfigResponseTests.swift */, A1A916E429C9191900D59D9E /* InAppConfigStub.swift */, F31470812B9634E000E01E5C /* InAppTargetingRequestsTests.swift */, - F3AF0ACF2BC40FCF0063BA58 /* InappTTLTests.swift */, ); path = InAppConfigResponseTests; sourceTree = ""; @@ -2119,14 +2113,6 @@ path = ConfigTargetings; sourceTree = ""; }; - F315503D2BBB24D60072A071 /* Services */ = { - isa = PBXGroup; - children = ( - F315503E2BBB24E20072A071 /* TTLValidationService.swift */, - ); - path = Services; - sourceTree = ""; - }; F31801F92A386A350021774C /* ConfigResponse */ = { isa = PBXGroup; children = ( @@ -3186,7 +3172,6 @@ 9B24FAB528C751E400F10B5D /* InAppImagesStorage.swift in Sources */, F3A8B9A32A3A6E6900E9C055 /* SdkVersionModel.swift in Sources */, 3333C1DE2681E9F300B60D84 /* URLRequestBuilder.swift in Sources */, - F315503F2BBB24E20072A071 /* TTLValidationService.swift in Sources */, 334F3AF5264C199900A6AC00 /* CodableDictionary.swift in Sources */, F3F5BB8A2B79F2600022AC3F /* PushNotificationFormatter.swift in Sources */, 334F3AF3264C199900A6AC00 /* AreaRequest.swift in Sources */, @@ -3425,7 +3410,6 @@ 9BC24E7728F6BF8C00C2619C /* InAppConfigResponseTests.swift in Sources */, F391170B2AB2E74F00852298 /* InappFilterServiceTests.swift in Sources */, F39B67AC2A3C7315005C0CCA /* MockImageDownloadService.swift in Sources */, - F3AF0AD02BC40FCF0063BA58 /* InappTTLTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Mindbox/DI/DependencyContainer.swift b/Mindbox/DI/DependencyContainer.swift index 8b57805c..ddd07e33 100644 --- a/Mindbox/DI/DependencyContainer.swift +++ b/Mindbox/DI/DependencyContainer.swift @@ -31,7 +31,6 @@ protocol DependencyContainer { var pushValidator: MindboxPushValidator { get } var inAppConfigurationDataFacade: InAppConfigurationDataFacadeProtocol { get } var userVisitManager: UserVisitManager { get } - var ttlValidationService: TTLValidationProtocol { get } } protocol InstanceFactory { diff --git a/Mindbox/DI/DependencyProvider.swift b/Mindbox/DI/DependencyProvider.swift index de696384..0662b0b1 100644 --- a/Mindbox/DI/DependencyProvider.swift +++ b/Mindbox/DI/DependencyProvider.swift @@ -33,7 +33,6 @@ final class DependencyProvider: DependencyContainer { var pushValidator: MindboxPushValidator var inAppConfigurationDataFacade: InAppConfigurationDataFacadeProtocol var userVisitManager: UserVisitManager - var ttlValidationService: TTLValidationProtocol init() throws { utilitiesFetcher = MBUtilitiesFetcher() @@ -79,7 +78,6 @@ final class DependencyProvider: DependencyContainer { let positionFilter = ElementsPositionFilterService() let elementsFilterService = ElementsFilterService(sizeFilter: sizeFilter, positionFilter: positionFilter, colorFilter: colorFilter) let contentPositionFilterService = ContentPositionFilterService() - let variantsFilterService = VariantFilterService(layersFilter: layersFilterService, elementsFilter: elementsFilterService, contentPositionFilter: contentPositionFilterService) @@ -89,7 +87,6 @@ final class DependencyProvider: DependencyContainer { variantsFilter: variantsFilterService, sdkVersionValidator: sdkVersionValidator) - ttlValidationService = TTLValidationService(persistenceStorage: persistenceStorage) inAppConfigurationDataFacade = InAppConfigurationDataFacade(geoService: geoService, segmentationService: segmentationSevice, targetingChecker: inAppTargetingChecker, @@ -104,9 +101,7 @@ final class DependencyProvider: DependencyContainer { targetingChecker: inAppTargetingChecker, urlExtractorService: urlExtractorService, dataFacade: inAppConfigurationDataFacade), - logsManager: logsManager, - persistenceStorage: persistenceStorage, - ttlValidationService: ttlValidationService), + logsManager: logsManager), presentationManager: presentationManager, persistenceStorage: persistenceStorage ) diff --git a/Mindbox/InAppMessages/Configuration/InAppConfigurationManager.swift b/Mindbox/InAppMessages/Configuration/InAppConfigurationManager.swift index c898ae6d..b1a585a8 100644 --- a/Mindbox/InAppMessages/Configuration/InAppConfigurationManager.swift +++ b/Mindbox/InAppMessages/Configuration/InAppConfigurationManager.swift @@ -33,23 +33,17 @@ class InAppConfigurationManager: InAppConfigurationManagerProtocol { private let inAppConfigurationMapper: InAppConfigutationMapper private let inAppConfigAPI: InAppConfigurationAPI private let logsManager: SDKLogsManagerProtocol - private let persistenceStorage: PersistenceStorage - private let ttlValidationService: TTLValidationProtocol init( inAppConfigAPI: InAppConfigurationAPI, inAppConfigRepository: InAppConfigurationRepository, inAppConfigurationMapper: InAppConfigutationMapper, - logsManager: SDKLogsManagerProtocol, - persistenceStorage: PersistenceStorage, - ttlValidationService: TTLValidationProtocol + logsManager: SDKLogsManagerProtocol ) { self.inAppConfigRepository = inAppConfigRepository self.inAppConfigurationMapper = inAppConfigurationMapper self.inAppConfigAPI = inAppConfigAPI self.logsManager = logsManager - self.persistenceStorage = persistenceStorage - self.ttlValidationService = ttlValidationService } weak var delegate: InAppConfigurationDelegate? @@ -115,16 +109,9 @@ class InAppConfigurationManager: InAppConfigurationManagerProtocol { } private func applyConfigFromCache() { - guard var cachedConfig = self.fetchConfigFromCache() else { - Logger.common(message: "Failed to apply configuration from cache: No cached configuration found.") + guard let cachedConfig = self.fetchConfigFromCache() else { return } - - if ttlValidationService.needResetInapps(config: cachedConfig) { - cachedConfig.inapps = nil - Logger.common(message: "[TTL] Resetting in-app due to the expiration of the current configuration.") - } - setConfigPrepared(cachedConfig) } @@ -142,9 +129,6 @@ class InAppConfigurationManager: InAppConfigurationManagerProtocol { } private func saveConfigToCache(_ data: Data) { - let now = Date() - persistenceStorage.configDownloadDate = now - Logger.common(message: "[TTL] Config download date successfully updated to: \(now.asDateTimeWithSeconds).") inAppConfigRepository.saveConfigToCache(data) } diff --git a/Mindbox/InAppMessages/Configuration/Services/TTLValidationService.swift b/Mindbox/InAppMessages/Configuration/Services/TTLValidationService.swift deleted file mode 100644 index a7e394bb..00000000 --- a/Mindbox/InAppMessages/Configuration/Services/TTLValidationService.swift +++ /dev/null @@ -1,76 +0,0 @@ -// -// TTLValidationService.swift -// Mindbox -// -// Created by vailence on 29.03.2024. -// Copyright © 2024 Mindbox. All rights reserved. -// - -import Foundation -import MindboxLogger - -protocol TTLValidationProtocol { - func needResetInapps(config: ConfigResponse) -> Bool -} - -class TTLValidationService: TTLValidationProtocol { - - let persistenceStorage: PersistenceStorage - - init(persistenceStorage: PersistenceStorage) { - self.persistenceStorage = persistenceStorage - } - - func needResetInapps(config: ConfigResponse) -> Bool { - guard let configDownloadDate = persistenceStorage.configDownloadDate else { - Logger.common(message: "[TTL] Config download date is nil. Unable to proceed with inapps reset validation.") - return false - } - - let now = Date() - - guard let ttl = config.settings?.ttl?.inapps, - let downloadConfigDateWithTTL = getDateWithIntervalByType(ttl: ttl, date: configDownloadDate) else { - Logger.common(message: "[TTL] Variables are missing or corrupted. Inapps reset will not be performed.") - return false - } - - let calendar = Calendar.current - let nowComponents = calendar.dateComponents([.year, .month, .day, .hour, .minute, .second], from: now) - let ttlComponents = calendar.dateComponents([.year, .month, .day, .hour, .minute, .second], from: downloadConfigDateWithTTL) - - guard let nowWithoutMilliseconds = calendar.date(from: nowComponents), - let downloadConfigDateWithTTLWithoutMilliseconds = calendar.date(from: ttlComponents) else { - Logger.common(message: "[TTL] Error in date components. Inapps reset will not be performed.") - return false - } - - let message = """ - [TTL] Current date: \(nowWithoutMilliseconds.asDateTimeWithSeconds). - Config with TTL valid until: \(downloadConfigDateWithTTLWithoutMilliseconds.asDateTimeWithSeconds). - Need to reset inapps: \(nowWithoutMilliseconds > downloadConfigDateWithTTLWithoutMilliseconds). - """ - - Logger.common(message: message) - return nowWithoutMilliseconds > downloadConfigDateWithTTLWithoutMilliseconds - } - - private func getDateWithIntervalByType(ttl: Settings.TimeToLive.TTLUnit, date: Date) -> Date? { - guard let type = ttl.unit, let value = ttl.value else { - Logger.common(message: "[TTL] Unable to calculate the date with TTL. The unit or value is missing.") - return nil - } - - let calendar = Calendar.current - switch type { - case .seconds: - return calendar.date(byAdding: .second, value: value, to: date) - case .minutes: - return calendar.date(byAdding: .minute, value: value, to: date) - case .hours: - return calendar.date(byAdding: .hour, value: value, to: date) - case .days: - return calendar.date(byAdding: .day, value: value, to: date) - } - } -} diff --git a/Mindbox/InAppMessages/Models/Config/ConfigResponse.swift b/Mindbox/InAppMessages/Models/Config/ConfigResponse.swift index 35cbcf3d..d6f39ad4 100644 --- a/Mindbox/InAppMessages/Models/Config/ConfigResponse.swift +++ b/Mindbox/InAppMessages/Models/Config/ConfigResponse.swift @@ -9,7 +9,7 @@ import Foundation import MindboxLogger struct ConfigResponse: Decodable { - var inapps: FailableDecodableArray? + let inapps: FailableDecodableArray? let monitoring: Monitoring? let settings: Settings? let abtests: [ABTest]? diff --git a/Mindbox/InAppMessages/Models/Config/SettingsModel.swift b/Mindbox/InAppMessages/Models/Config/SettingsModel.swift index 62fa5e7d..116400dc 100644 --- a/Mindbox/InAppMessages/Models/Config/SettingsModel.swift +++ b/Mindbox/InAppMessages/Models/Config/SettingsModel.swift @@ -10,7 +10,6 @@ import Foundation struct Settings: Decodable, Equatable { let operations: SettingsOperations? - let ttl: TimeToLive? struct SettingsOperations: Decodable, Equatable { @@ -22,38 +21,4 @@ struct Settings: Decodable, Equatable { let systemName: String } } - - struct TimeToLive: Decodable, Equatable { - let inapps: TTLUnit? - } -} - -extension Settings.TimeToLive { - struct TTLUnit: Decodable, Equatable { - let unit: Unit? - let value: Int? - - enum Unit: String, Decodable { - case seconds - case minutes - case hours - case days - } - - init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - value = try? container.decode(Int.self, forKey: .value) - let unitString = try? container.decode(String.self, forKey: .unit).lowercased() - self.unit = Unit(rawValue: unitString ?? "") - } - - init(unit: Unit?, value: Int?) { - self.unit = unit - self.value = value - } - - private enum CodingKeys: String, CodingKey { - case unit, value - } - } } diff --git a/Mindbox/Info.plist b/Mindbox/Info.plist index 181b92ca..2c85542f 100644 --- a/Mindbox/Info.plist +++ b/Mindbox/Info.plist @@ -17,6 +17,6 @@ CFBundleShortVersionString $(MARKETING_VERSION) CFBundleVersion - 5364 + 5351 diff --git a/Mindbox/PersistenceStorage/MBPersistenceStorage.swift b/Mindbox/PersistenceStorage/MBPersistenceStorage.swift index 28ecbfea..3765d45f 100644 --- a/Mindbox/PersistenceStorage/MBPersistenceStorage.swift +++ b/Mindbox/PersistenceStorage/MBPersistenceStorage.swift @@ -95,23 +95,6 @@ class MBPersistenceStorage: PersistenceStorage { } } - var configDownloadDate: Date? { - get { - if let dateString = configDownloadDateString { - return dateFormatter.date(from: dateString) - } else { - return nil - } - } - set { - if let date = newValue { - configDownloadDateString = dateFormatter.string(from: date) - } else { - configDownloadDateString = nil - } - } - } - var backgroundExecutions: [BackgroudExecution] { get { if let data = MBPersistenceStorage.defaults.value(forKey:"backgroundExecution") as? Data { @@ -233,13 +216,6 @@ class MBPersistenceStorage: PersistenceStorage { onDidChange?() } } - - @UserDefaultsWrapper(key: .configDownloadDate, defaultValue: nil) - private var configDownloadDateString: String? { - didSet { - onDidChange?() - } - } func reset() { installationDate = nil @@ -250,7 +226,6 @@ class MBPersistenceStorage: PersistenceStorage { deprecatedEventsRemoveDate = nil configuration = nil isNotificationsEnabled = nil - configDownloadDate = nil resetBackgroundExecutions() } @@ -293,7 +268,6 @@ extension MBPersistenceStorage { case imageLoadingMaxTimeInSeconds = "MBPersistenceStorage-imageLoadingMaxTimeInSeconds" case needUpdateInfoOnce = "MBPersistenceStorage-needUpdateInfoOnce" case userVisitCount = "MBPersistenceStorage-userVisitCount" - case configDownloadDate = "MBPersistenceStorage-configDownloadDate" } private let key: Key diff --git a/Mindbox/PersistenceStorage/PersistenceStorage.swift b/Mindbox/PersistenceStorage/PersistenceStorage.swift index 5effe32f..afb807df 100644 --- a/Mindbox/PersistenceStorage/PersistenceStorage.swift +++ b/Mindbox/PersistenceStorage/PersistenceStorage.swift @@ -48,6 +48,4 @@ protocol PersistenceStorage: AnyObject { var needUpdateInfoOnce: Bool? { get set } var userVisitCount: Int? { get set } - - var configDownloadDate: Date? { get set } } diff --git a/MindboxTests/DI/TestDependencyProvider.swift b/MindboxTests/DI/TestDependencyProvider.swift index b1996e8e..fc491e7e 100644 --- a/MindboxTests/DI/TestDependencyProvider.swift +++ b/MindboxTests/DI/TestDependencyProvider.swift @@ -34,7 +34,6 @@ final class TestDependencyProvider: DependencyContainer { var pushValidator: MindboxPushValidator var inAppConfigurationDataFacade: InAppConfigurationDataFacadeProtocol var userVisitManager: UserVisitManager - var ttlValidationService: TTLValidationProtocol init() throws { utilitiesFetcher = MBUtilitiesFetcher() @@ -66,7 +65,6 @@ final class TestDependencyProvider: DependencyContainer { imageDownloadService = MockImageDownloadService() abTestDeviceMixer = ABTestDeviceMixer() urlExtractorService = VariantImageUrlExtractorService() - ttlValidationService = TTLValidationService(persistenceStorage: persistenceStorage) let tracker = InAppMessagesTracker(databaseRepository: databaseRepository) inAppConfigurationDataFacade = InAppConfigurationDataFacade(geoService: geoService, segmentationService: segmentationSevice, diff --git a/MindboxTests/InApp/Tests/InAppConfigResponseTests/InappTTLTests.swift b/MindboxTests/InApp/Tests/InAppConfigResponseTests/InappTTLTests.swift deleted file mode 100644 index af22facd..00000000 --- a/MindboxTests/InApp/Tests/InAppConfigResponseTests/InappTTLTests.swift +++ /dev/null @@ -1,75 +0,0 @@ -// -// InappTTLTests.swift -// MindboxTests -// -// Created by vailence on 08.04.2024. -// Copyright © 2024 Mindbox. All rights reserved. -// - -import XCTest -@testable import Mindbox - -class InappTTLTests: XCTestCase { - var container: TestDependencyProvider! - var persistenceStorage: PersistenceStorage! - var service: TTLValidationProtocol! - - override func setUp() { - super.setUp() - container = try! TestDependencyProvider() - persistenceStorage = container.persistenceStorage - service = container.ttlValidationService - } - - override func tearDown() { - container = nil - persistenceStorage = nil - service = nil - super.tearDown() - } - - func testNeedResetInapps_WithTTL_Exceeds() throws { - persistenceStorage.configDownloadDate = Calendar.current.date(byAdding: .hour, value: -2, to: Date()) - let service = TTLValidationService(persistenceStorage: persistenceStorage) - let settings = Settings(operations: nil, ttl: .init(inapps: .init(unit: .hours, value: 1))) - let config = ConfigResponse(settings: settings) - let result = service.needResetInapps(config: config) - XCTAssertTrue(result, "Inapps должны быть сброшены, так как время ttl истекло.") - } - - func testNeedResetInapps_WithTTL_NotExceeded() throws { - persistenceStorage.configDownloadDate = Calendar.current.date(byAdding: .second, value: -1, to: Date()) - let service = TTLValidationService(persistenceStorage: persistenceStorage) - let settings = Settings(operations: nil, ttl: .init(inapps: .init(unit: .seconds, value: 1))) - let config = ConfigResponse(settings: settings) - let result = service.needResetInapps(config: config) - XCTAssertFalse(result, "Inapps не должны быть сброшены, так как время ttl еще не истекло.") - } - - func testNeedResetInapps_WithoutTTL() throws { - persistenceStorage.configDownloadDate = Date() - let service = TTLValidationService(persistenceStorage: persistenceStorage) - let settings = Settings(operations: nil, ttl: nil) - let config = ConfigResponse(settings: settings) - let result = service.needResetInapps(config: config) - XCTAssertFalse(result, "Inapps не должны быть сброшены, так как в конфиге отсутствует TTL.") - } - - func testNeedResetInapps_ExactlyAtTTL_ShouldNotReset() throws { - let service = TTLValidationService(persistenceStorage: persistenceStorage) - let settings = Settings(operations: nil, ttl: .init(inapps: .init(unit: .seconds, value: 1))) - let config = ConfigResponse(settings: settings) - persistenceStorage.configDownloadDate = Calendar.current.date(byAdding: .second, value: -1, to: Date()) - let result = service.needResetInapps(config: config) - XCTAssertFalse(result, "Inapps не должны сбрасываться, если текущее время точно совпадает с истечением TTL.") - } - - func testNeedResetInapps_WithTTLHalfHourAgo_NotExceeded() throws { - persistenceStorage.configDownloadDate = Calendar.current.date(byAdding: .minute, value: -30, to: Date()) - let service = TTLValidationService(persistenceStorage: persistenceStorage) - let settings = Settings(operations: nil, ttl: .init(inapps: .init(unit: .hours, value: 1))) - let config = ConfigResponse(settings: settings) - let result = service.needResetInapps(config: config) - XCTAssertFalse(result, "Inapps не должны быть сброшены, так как время TTL еще не истекло.") - } -} diff --git a/MindboxTests/Mock/MockPersistenceStorage.swift b/MindboxTests/Mock/MockPersistenceStorage.swift index 1488d223..2ffabf2e 100644 --- a/MindboxTests/Mock/MockPersistenceStorage.swift +++ b/MindboxTests/Mock/MockPersistenceStorage.swift @@ -88,12 +88,6 @@ class MockPersistenceStorage: PersistenceStorage { set { _userVisitCount = newValue } } - var configDownloadDate: Date? { - didSet { - onDidChange?() - } - } - func reset() { installationDate = nil deviceUUID = nil From c50be5550fd5a624a51adfd4bf1d08120d3a7967 Mon Sep 17 00:00:00 2001 From: Akylbek Utekeshev Date: Thu, 18 Apr 2024 13:00:55 +0500 Subject: [PATCH 34/35] Revert "Merge pull request #350 from mindbox-cloud/feature/MBX-3245-TTL-fix" This reverts commit dcd69afc5cc5a1a6533c18e4a58aef087c2a802e, reversing changes made to 741693cfa5bbaf1bf5a09b796644d7975be9ef39. --- .../API/InAppConfigurationAPI.swift | 2 +- Mindbox/Model/Common/MBDate.swift | 17 ----------------- 2 files changed, 1 insertion(+), 18 deletions(-) diff --git a/Mindbox/InAppMessages/Configuration/API/InAppConfigurationAPI.swift b/Mindbox/InAppMessages/Configuration/API/InAppConfigurationAPI.swift index fa58e556..e70fefb8 100644 --- a/Mindbox/InAppMessages/Configuration/API/InAppConfigurationAPI.swift +++ b/Mindbox/InAppMessages/Configuration/API/InAppConfigurationAPI.swift @@ -36,7 +36,7 @@ class InAppConfigurationAPI { let builder = URLRequestBuilder(domain: configuration.domain) var urlRequest = try builder.asURLRequest(route: route) Logger.network(request: urlRequest) - urlRequest.cachePolicy = .reloadIgnoringLocalAndRemoteCacheData + urlRequest.cachePolicy = .useProtocolCachePolicy URLSession.shared.dataTask(with: urlRequest) { [self] data, response, error in completionQueue.async { let result = self.completeDownloadTask(data, response: response, error: error) diff --git a/Mindbox/Model/Common/MBDate.swift b/Mindbox/Model/Common/MBDate.swift index ec622458..754fca7b 100644 --- a/Mindbox/Model/Common/MBDate.swift +++ b/Mindbox/Model/Common/MBDate.swift @@ -79,10 +79,6 @@ extension Date { public var asDateTime: DateTime { return DateTime(self) } - - public var asDateTimeWithSeconds: String { - return DateTimeWithSeconds(self).string - } } fileprivate extension Date { @@ -112,16 +108,3 @@ fileprivate extension String { return Date.Formatter.iso8601.date(from: data) } } - -public final class DateTimeWithSeconds: MBDate { - override var dateFormat: String { - "yyyy-MM-dd HH:mm:ss" - } - - override func decodeWithFormat(_ rawString: String) -> Date? { - let formatter = DateFormatter() - formatter.dateFormat = dateFormat - formatter.timeZone = TimeZone(secondsFromGMT: 0) - return formatter.date(from: rawString) - } -} From 640ffa38e532ea751f62518d6f01c974e49b4d37 Mon Sep 17 00:00:00 2001 From: Akylbek Utekeshev Date: Thu, 18 Apr 2024 19:36:29 +0500 Subject: [PATCH 35/35] Version UP to 2.9.0 --- Mindbox.podspec | 2 +- MindboxNotifications.podspec | 2 +- SDKVersionProvider/SDKVersionConfig.xcconfig | 2 +- SDKVersionProvider/SDKVersionProvider.swift | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Mindbox.podspec b/Mindbox.podspec index f1937caf..38bc11c4 100644 --- a/Mindbox.podspec +++ b/Mindbox.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |spec| spec.name = "Mindbox" - spec.version = "2.9.0-rc" + spec.version = "2.9.0" spec.summary = "SDK for integration with Mindbox" spec.description = "This library allows you to integrate data transfer to Mindbox Marketing Cloud" spec.homepage = "https://github.com/mindbox-cloud/ios-sdk" diff --git a/MindboxNotifications.podspec b/MindboxNotifications.podspec index e74ef05b..d0cec8d4 100644 --- a/MindboxNotifications.podspec +++ b/MindboxNotifications.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |spec| spec.name = "MindboxNotifications" - spec.version = "2.9.0-rc" + spec.version = "2.9.0" spec.summary = "SDK for integration notifications with Mindbox" spec.description = "This library allows you to integrate notifications and transfer them to Mindbox Marketing Cloud" spec.homepage = "https://github.com/mindbox-cloud/ios-sdk" diff --git a/SDKVersionProvider/SDKVersionConfig.xcconfig b/SDKVersionProvider/SDKVersionConfig.xcconfig index e46cb9a5..8502e56d 100644 --- a/SDKVersionProvider/SDKVersionConfig.xcconfig +++ b/SDKVersionProvider/SDKVersionConfig.xcconfig @@ -1 +1 @@ -MARKETING_VERSION = 2.9.0-rc +MARKETING_VERSION = 2.9.0 diff --git a/SDKVersionProvider/SDKVersionProvider.swift b/SDKVersionProvider/SDKVersionProvider.swift index 1453b80b..d12410ed 100644 --- a/SDKVersionProvider/SDKVersionProvider.swift +++ b/SDKVersionProvider/SDKVersionProvider.swift @@ -8,5 +8,5 @@ import Foundation public class SDKVersionProvider { - public static let sdkVersion = "2.9.0-rc" + public static let sdkVersion = "2.9.0" }