Skip to content

Commit

Permalink
MOBILE-4529: Add a new subscription list updates call that works simi…
Browse files Browse the repository at this point in the history
…larly to contact channels (#3144)

* wip

* feedback

* added contact manuall refresh

* fixes

* added unit tests

* Refresh on foreground

* Remove actor

* Wire up push refresh

* Tests

---------

Co-authored-by: Igor Litvinenko <[email protected]>
Co-authored-by: Ryan Lepinski <[email protected]>
  • Loading branch information
3 people authored Jun 28, 2024
1 parent 37125e1 commit c78874e
Show file tree
Hide file tree
Showing 11 changed files with 718 additions and 327 deletions.
24 changes: 24 additions & 0 deletions Airship/Airship.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,15 @@
607951402A1E74AC0086578F /* MessageCriteria.swift in Sources */ = {isa = PBXBuildFile; fileRef = 60D3BCCF2A154D9400E07524 /* MessageCriteria.swift */; };
607951412A1E74AC0086578F /* MessageCriteria.swift in Sources */ = {isa = PBXBuildFile; fileRef = 60D3BCCF2A154D9400E07524 /* MessageCriteria.swift */; };
6087DB882B278F7600449BA8 /* JsonValueMatcherTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6087DB872B278F7600449BA8 /* JsonValueMatcherTest.swift */; };
608B16E62C2C1138005298FA /* SubscriptionListProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 608B16E52C2C1137005298FA /* SubscriptionListProvider.swift */; };
608B16E82C2C6DDB005298FA /* ChannelSubscriptionListProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 608B16E72C2C6DDB005298FA /* ChannelSubscriptionListProvider.swift */; };
608B16EB2C2C77B0005298FA /* SubscriptionListProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 608B16E52C2C1137005298FA /* SubscriptionListProvider.swift */; };
608B16EC2C2C77B1005298FA /* SubscriptionListProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 608B16E52C2C1137005298FA /* SubscriptionListProvider.swift */; };
608B16ED2C2C77B4005298FA /* ChannelSubscriptionListProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 608B16E72C2C6DDB005298FA /* ChannelSubscriptionListProvider.swift */; };
608B16EE2C2C77B4005298FA /* ChannelSubscriptionListProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 608B16E72C2C6DDB005298FA /* ChannelSubscriptionListProvider.swift */; };
608B16F12C2D5F0D005298FA /* BaseCachingRemoteDataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 608B16F02C2D5F0D005298FA /* BaseCachingRemoteDataProvider.swift */; };
608B16F22C2D5F0D005298FA /* BaseCachingRemoteDataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 608B16F02C2D5F0D005298FA /* BaseCachingRemoteDataProvider.swift */; };
608B16F32C2D5F0D005298FA /* BaseCachingRemoteDataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 608B16F02C2D5F0D005298FA /* BaseCachingRemoteDataProvider.swift */; };
60A5CC082B28DC500017EDB2 /* NotificationCategoriesTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 60A5CC072B28DC500017EDB2 /* NotificationCategoriesTest.swift */; };
60A5CC0C2B29AE890017EDB2 /* ProximityRegionTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 60A5CC0B2B29AE890017EDB2 /* ProximityRegionTest.swift */; };
60A5CC0E2B29B1B80017EDB2 /* CircularRegionTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 60A5CC0D2B29B1B80017EDB2 /* CircularRegionTest.swift */; };
Expand Down Expand Up @@ -2049,6 +2058,9 @@
6068E03A2B2CBCF200349E82 /* ActiveTimer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActiveTimer.swift; sourceTree = "<group>"; };
6079511F2A1CD19F0086578F /* ExperimentManagerTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExperimentManagerTest.swift; sourceTree = "<group>"; };
6087DB872B278F7600449BA8 /* JsonValueMatcherTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JsonValueMatcherTest.swift; sourceTree = "<group>"; };
608B16E52C2C1137005298FA /* SubscriptionListProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionListProvider.swift; sourceTree = "<group>"; };
608B16E72C2C6DDB005298FA /* ChannelSubscriptionListProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelSubscriptionListProvider.swift; sourceTree = "<group>"; };
608B16F02C2D5F0D005298FA /* BaseCachingRemoteDataProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BaseCachingRemoteDataProvider.swift; sourceTree = "<group>"; };
60A5CC072B28DC500017EDB2 /* NotificationCategoriesTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationCategoriesTest.swift; sourceTree = "<group>"; };
60A5CC0B2B29AE890017EDB2 /* ProximityRegionTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProximityRegionTest.swift; sourceTree = "<group>"; };
60A5CC0D2B29B1B80017EDB2 /* CircularRegionTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CircularRegionTest.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -3922,6 +3934,7 @@
6ED735D926C73DC5003B0A7D /* Channel.swift */,
6ED735DC26C7401D003B0A7D /* TagEditor.swift */,
E976486E27A46CC50024518D /* ChannelType.swift */,
608B16E72C2C6DDB005298FA /* ChannelSubscriptionListProvider.swift */,
);
name = Channel;
sourceTree = "<group>";
Expand All @@ -3936,6 +3949,7 @@
6E411D282538CF0500FEE4E8 /* Contacts */ = {
isa = PBXGroup;
children = (
608B16F02C2D5F0D005298FA /* BaseCachingRemoteDataProvider.swift */,
E9343E1B27AC413700A49AAF /* AssociatedChannel.swift */,
A67F87D1268DECCE00EF5F43 /* ContactAPIClient.swift */,
6EB11C862697ACBF00DC698F /* ContactOperation.swift */,
Expand All @@ -3953,6 +3967,7 @@
6E60EF6929DF542B003F7A8D /* AnonContactData.swift */,
6E1EEE8F2BD81AF300B45A87 /* ContactChannel.swift */,
990EB3B02BF59A1500315EAC /* ContactChannelsProvider.swift */,
608B16E52C2C1137005298FA /* SubscriptionListProvider.swift */,
);
name = Contacts;
sourceTree = "<group>";
Expand Down Expand Up @@ -6109,6 +6124,7 @@
6E91E44F28EF423400B6F25E /* AirshipWorkerType.swift in Sources */,
6EB5158328A47C7100870C5A /* ScopedSubscriptionListEdit.swift in Sources */,
60D3BCD02A154D9400E07524 /* MessageCriteria.swift in Sources */,
608B16E62C2C1138005298FA /* SubscriptionListProvider.swift in Sources */,
6E94761529BBC0240025F364 /* AirshipButton.swift in Sources */,
3251586B272AFB2E00DF8B44 /* MediaWebView.swift in Sources */,
6E96ED02294115210053CC91 /* AsyncStream.swift in Sources */,
Expand Down Expand Up @@ -6162,6 +6178,7 @@
6EFD6D6E27290C0B005B26F1 /* FormController.swift in Sources */,
6E91E44C28EF423400B6F25E /* AirshipWorkRequest.swift in Sources */,
6EE49BDD2A09AD3600AB1CF4 /* AirshipNotificationStatus.swift in Sources */,
608B16F12C2D5F0D005298FA /* BaseCachingRemoteDataProvider.swift in Sources */,
6EFD6D82272A53AE005B26F1 /* PagerState.swift in Sources */,
6E9D529B26C1A77C004EA16B /* ActionRegistry.swift in Sources */,
6E87BE0726E283850005D20D /* DeepLinkDelegate.swift in Sources */,
Expand Down Expand Up @@ -6221,6 +6238,7 @@
6EF27DE62730E77300548DA3 /* RadioInputState.swift in Sources */,
6EB56C7C27AC4BBB00A7392F /* AssociatedChannel.swift in Sources */,
6E91E44328EF423400B6F25E /* WorkRateLimiterActor.swift in Sources */,
608B16E82C2C6DDB005298FA /* ChannelSubscriptionListProvider.swift in Sources */,
6ECB627E2A36A0770095C85C /* ExternalURLProcessor.swift in Sources */,
6E1BACDD2719FC0A0038399E /* ViewFactory.swift in Sources */,
6E9B4874288F0CE000C905B1 /* RateAppAction.swift in Sources */,
Expand Down Expand Up @@ -6829,6 +6847,7 @@
A6722A80281A9EDA0033F54D /* ContactOperation.swift in Sources */,
A6722A83281A9EDA0033F54D /* ContactConflictEvent.swift in Sources */,
A6722A84281A9EDA0033F54D /* AirshipContact.swift in Sources */,
608B16F32C2D5F0D005298FA /* BaseCachingRemoteDataProvider.swift in Sources */,
A6722A86281A9EDA0033F54D /* EmailRegistrationOptions.swift in Sources */,
6E1589562AFF021D00954A04 /* SessionState.swift in Sources */,
6E91E43C28EF423400B6F25E /* WorkBackgroundTasks.swift in Sources */,
Expand Down Expand Up @@ -6907,6 +6926,7 @@
A6722A48281A9EB80033F54D /* CircularRegion.swift in Sources */,
A6722A4A281A9EB80033F54D /* EventAPIClient.swift in Sources */,
A6722A4C281A9EB80033F54D /* EventUtils.swift in Sources */,
608B16EE2C2C77B4005298FA /* ChannelSubscriptionListProvider.swift in Sources */,
6E7112AB2881D160004942E4 /* VisibilityViewModifier.swift in Sources */,
6E5A64D62AABBED600574085 /* MeteredUsageStore.swift in Sources */,
A6722A4F281A9EB80033F54D /* MediaEventTemplate.swift in Sources */,
Expand Down Expand Up @@ -7005,6 +7025,7 @@
6E5A64D22AABBEAF00574085 /* AirshipMeteredUsageEvent.swift in Sources */,
6EE49C242A13E32B00AB1CF4 /* RemoteDataProviderProtocol.swift in Sources */,
6EC3670B2AD8A8A400355D11 /* AirshipEmbeddedObserver.swift in Sources */,
608B16EC2C2C77B1005298FA /* SubscriptionListProvider.swift in Sources */,
6ECDDE7629B80462009D79DB /* ChannelAuthTokenProvider.swift in Sources */,
6E5A64DB2AABC5A400574085 /* UAMeteredUsage.xcdatamodeld in Sources */,
6EC367072AD8A8A400355D11 /* EmbeddedView.swift in Sources */,
Expand Down Expand Up @@ -7309,6 +7330,7 @@
6E94761629BBC0240025F364 /* AirshipButton.swift in Sources */,
6E664BDE26C4CD8700A2C8E5 /* OpenExternalURLAction.swift in Sources */,
6E15B71926CEB4190099C92D /* RemoteDataStore.swift in Sources */,
608B16EB2C2C77B0005298FA /* SubscriptionListProvider.swift in Sources */,
6E96ED03294115210053CC91 /* AsyncStream.swift in Sources */,
6E739D6C26B9DFFB00BC6F6D /* AttributePendingMutations.swift in Sources */,
6E87BDBE26E01FF40005D20D /* ModuleLoader.swift in Sources */,
Expand Down Expand Up @@ -7362,6 +7384,7 @@
6E698DED26790AC300654DB2 /* PreferenceDataStore.swift in Sources */,
E99605A227A071EA00365AE4 /* EmailRegistrationOptions.swift in Sources */,
6EFD6D6F27290C0B005B26F1 /* FormController.swift in Sources */,
608B16F22C2D5F0D005298FA /* BaseCachingRemoteDataProvider.swift in Sources */,
6E87BE0826E283850005D20D /* DeepLinkDelegate.swift in Sources */,
6E91E44D28EF423400B6F25E /* AirshipWorkRequest.swift in Sources */,
6EE49BDE2A09AD3600AB1CF4 /* AirshipNotificationStatus.swift in Sources */,
Expand Down Expand Up @@ -7421,6 +7444,7 @@
6E96ECF7293FCE080053CC91 /* EventUploadScheduler.swift in Sources */,
6EF27DE72730E77300548DA3 /* RadioInputState.swift in Sources */,
6E71129E2880DACB004942E4 /* EventHandlerViewModifier.swift in Sources */,
608B16ED2C2C77B4005298FA /* ChannelSubscriptionListProvider.swift in Sources */,
A6F6726126E1162F008C69C3 /* JSONMatcher.swift in Sources */,
6E698DF126790AC300654DB2 /* AirshipLogger.swift in Sources */,
6EB56C7D27AC4BBC00A7392F /* AssociatedChannel.swift in Sources */,
Expand Down
73 changes: 29 additions & 44 deletions Airship/AirshipCore/Source/AirshipContact.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import Foundation
/// within Airship. Contacts may be named and have channels associated with it.
@objc(UAContact)
public final class AirshipContact: NSObject, AirshipContactProtocol, @unchecked Sendable {
static let refreshContactPushPayloadKey = "com.urbanairship.contact.update"

public var contactChannelUpdates: AsyncStream<ContactChannelsResult> {
get {
return self.contactChannelsProvider.contactChannels(
Expand Down Expand Up @@ -68,16 +70,15 @@ public final class AirshipContact: NSObject, AirshipContactProtocol, @unchecked
private let dataStore: PreferenceDataStore
private let config: RuntimeConfig
private let privacyManager: AirshipPrivacyManager
private let subscriptionListAPIClient: ContactSubscriptionListAPIClientProtocol
private let contactChannelsProvider: ContactChannelsProviderProtocol
private let subscriptionListProvider: SubscriptionListProviderProtocol
private let date: AirshipDateProtocol
private let audienceOverridesProvider: AudienceOverridesProvider
private let contactManager: ContactManagerProtocol
private var smsValidator: SMSValidatorProtocol
private let cachedSubscriptionLists: CachedValue<(String, [String: [ChannelScope]])>
private var setupTask: Task<Void, Never>? = nil
private var subscriptions: Set<AnyCancellable> = Set()
private let fetchSubscriptionListQueue: AirshipSerialQueue = AirshipSerialQueue()
private let serialQueue: AirshipAsyncSerialQueue

/// Publishes all edits made to the subscription lists through the SDK
Expand Down Expand Up @@ -159,8 +160,8 @@ public final class AirshipContact: NSObject, AirshipContactProtocol, @unchecked
config: RuntimeConfig,
channel: InternalAirshipChannelProtocol,
privacyManager: AirshipPrivacyManager,
subscriptionListAPIClient: ContactSubscriptionListAPIClientProtocol,
contactChannelsProvider: ContactChannelsProviderProtocol,
subscriptionListProvider: SubscriptionListProviderProtocol,
date: AirshipDateProtocol = AirshipDate.shared,
notificationCenter: AirshipNotificationCenter = AirshipNotificationCenter.shared,
audienceOverridesProvider: AudienceOverridesProvider,
Expand All @@ -172,16 +173,15 @@ public final class AirshipContact: NSObject, AirshipContactProtocol, @unchecked
self.dataStore = dataStore
self.config = config
self.privacyManager = privacyManager
self.subscriptionListAPIClient = subscriptionListAPIClient
self.contactChannelsProvider = contactChannelsProvider
self.audienceOverridesProvider = audienceOverridesProvider
self.date = date
self.contactManager = contactManager
self.smsValidator = smsValidator
self.serialQueue = serialQueue

self.subscriptionListProvider = subscriptionListProvider
self.cachedSubscriptionLists = CachedValue(date: date)

super.init()

self.setupTask = Task {
Expand Down Expand Up @@ -330,12 +330,15 @@ public final class AirshipContact: NSObject, AirshipContactProtocol, @unchecked
config: config,
channel: channel,
privacyManager: privacyManager,
subscriptionListAPIClient: ContactSubscriptionListAPIClient(config: config),
contactChannelsProvider: ContactChannelsProvider(
audienceOverrides: audienceOverridesProvider,
apiClient: ContactChannelsAPIClient(config: config),
privacyManager: privacyManager
),
subscriptionListProvider: SubscriptionListProvider(
audienceOverrides: audienceOverridesProvider,
apiClient: ContactSubscriptionListAPIClient(config: config),
privacyManager: privacyManager),
audienceOverridesProvider: audienceOverridesProvider,
contactManager: ContactManager(
dataStore: dataStore,
Expand Down Expand Up @@ -715,43 +718,7 @@ public final class AirshipContact: NSObject, AirshipContactProtocol, @unchecked

public func fetchSubscriptionLists() async throws -> [String: [ChannelScope]] {
let contactID = await getStableContactID()
var subscriptions = try await self.resolveSubscriptionLists(contactID)

let overrides = await self.audienceOverridesProvider.contactOverrides(contactID: contactID)

subscriptions = AudienceUtils.applySubscriptionListsUpdates(
subscriptions,
updates: overrides.subscriptionLists
)

return subscriptions
}

private func resolveSubscriptionLists(
_ contactID: String
) async throws -> [String: [ChannelScope]] {

return try await self.fetchSubscriptionListQueue.run {
if let cached = self.cachedSubscriptionLists.value,
cached.0 == contactID {
return cached.1
}

let response = try await self.subscriptionListAPIClient.fetchSubscriptionLists(
contactID: contactID
)

guard response.isSuccess, let lists = response.result else {
throw AirshipErrors.error("Failed to fetch subscription lists")
}

AirshipLogger.debug("Fetched lists finished with response: \(response)")
self.cachedSubscriptionLists.set(
value: (contactID, lists),
expiresIn: Self.maxSubscriptionListCacheAge
)
return lists
}
return try await subscriptionListProvider.fetch(contactID: contactID)
}

@objc
Expand All @@ -778,6 +745,8 @@ public final class AirshipContact: NSObject, AirshipContactProtocol, @unchecked
self.lastResolveDate = self.date.now
self.addOperation(.resolve)
}

self.contactChannelsProvider.refreshAsync()
}

@objc
Expand Down Expand Up @@ -927,6 +896,22 @@ extension AirshipContact : InternalAirshipContactProtocol {
}
}

#if !os(watchOS)
extension AirshipContact: AirshipPushableComponent {
public func receivedRemoteNotification(
_ notification: [AnyHashable: Any],
completionHandler: @escaping (UIBackgroundFetchResult) -> Void
) {
if notification[Self.refreshContactPushPayloadKey] == nil {
completionHandler(.noData)
} else {
self.contactChannelsProvider.refreshAsync()
completionHandler(.newData)
}
}
}
#endif



extension AirshipContact: AirshipComponent {}
Expand Down
Loading

0 comments on commit c78874e

Please sign in to comment.