Skip to content

Commit

Permalink
Merge branch 'next'
Browse files Browse the repository at this point in the history
  • Loading branch information
crow committed Jun 14, 2024
2 parents 80fe9bd + bda26ea commit 770b9c1
Show file tree
Hide file tree
Showing 227 changed files with 12,158 additions and 2,951 deletions.
18 changes: 9 additions & 9 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ concurrency:
jobs:
run-tests-core:
runs-on: macos-14-xlarge
timeout-minutes: 10
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- name: Install yeetd
Expand All @@ -25,7 +25,7 @@ jobs:

run-tests-message-center:
runs-on: macos-14-xlarge
timeout-minutes: 10
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- name: Install yeetd
Expand All @@ -38,7 +38,7 @@ jobs:

run-tests-preference-center:
runs-on: macos-14-xlarge
timeout-minutes: 10
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- name: Install yeetd
Expand All @@ -51,7 +51,7 @@ jobs:

run-tests-feature-flags:
runs-on: macos-14-xlarge
timeout-minutes: 10
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- name: Install yeetd
Expand All @@ -64,7 +64,7 @@ jobs:

run-tests-automation:
runs-on: macos-14-xlarge
timeout-minutes: 10
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- name: Install yeetd
Expand All @@ -77,7 +77,7 @@ jobs:

run-tests-extensions:
runs-on: macos-14-xlarge
timeout-minutes: 10
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- name: Install yeetd
Expand All @@ -90,7 +90,7 @@ jobs:

run-package-tests:
runs-on: macos-14-xlarge
timeout-minutes: 10
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- name: Install xcodegen
Expand All @@ -100,7 +100,7 @@ jobs:

build-samples:
runs-on: macos-14-xlarge
timeout-minutes: 10
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- name: Build samples
Expand Down Expand Up @@ -171,4 +171,4 @@ jobs:
# steps:
# - uses: actions/checkout@v4
# - name: Pod lint
# run: make pod-lint-visionos
# run: make pod-lint-visionos
2 changes: 1 addition & 1 deletion Airship.podspec
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
AIRSHIP_VERSION="18.3.1"
AIRSHIP_VERSION="18.4.0"

Pod::Spec.new do |s|
s.version = AIRSHIP_VERSION
Expand Down
3 changes: 2 additions & 1 deletion Airship.xcworkspace/xcshareddata/swiftpm/Package.resolved
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"originHash" : "15f70542934037e26c0fb164e8d9c1cad34f07aec670e7536cfde8fc58e7d90f",
"pins" : [
{
"identity" : "yams",
Expand All @@ -10,5 +11,5 @@
}
}
],
"version" : 2
"version" : 3
}
292 changes: 233 additions & 59 deletions Airship/Airship.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ struct ActionAutomationExecutor: AutomationExecutorDelegate {
}

func execute(data: AirshipJSON, preparedScheduleInfo: PreparedScheduleInfo) async -> ScheduleExecuteResult {
guard preparedScheduleInfo.additionalAudienceCheckResult else {
return .finished
}

await actionRunner.runActions(data, situation: .automation, metadata: [:])
return .finished
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/* Copyright Airship and Contributors */

import Foundation
#if canImport(AirshipCore)
import AirshipCore
#endif

protocol AdditionalAudienceCheckerAPIClientProtocol: Sendable {
func resolve(
info: AdditionalAudienceCheckResult.Request
) async throws -> AirshipHTTPResponse<AdditionalAudienceCheckResult>
}

struct AdditionalAudienceCheckResult: Codable, Sendable, Equatable {
let isMatched: Bool
let cacheTTL: TimeInterval

enum CodingKeys: String, CodingKey {
case isMatched = "allowed"
case cacheTTL = "cache_seconds"
}

struct Request: Sendable {
let url: URL
let channelID: String
let contactID: String
let namedUserID: String?
let context: AirshipJSON?
}
}

final class AdditionalAudienceCheckerAPIClient: AdditionalAudienceCheckerAPIClientProtocol {
private let config: RuntimeConfig
private let session: AirshipRequestSession
private let encoder: JSONEncoder

init(config: RuntimeConfig, session: AirshipRequestSession, encoder: JSONEncoder = JSONEncoder()) {
self.config = config
self.session = session
self.encoder = encoder
}

convenience init(config: RuntimeConfig) {
self.init(
config: config,
session: config.requestSession
)
}

func resolve(
info: AdditionalAudienceCheckResult.Request
) async throws -> AirshipHTTPResponse<AdditionalAudienceCheckResult> {

let body = RequestBody(
channelID: info.channelID,
contactID: info.contactID,
namedUserID: info.namedUserID,
context: info.context
)

let request = AirshipRequest(
url: info.url,
headers: [
"Content-Type": "application/json",
"Accept": "application/vnd.urbanairship+json; version=3;",
"X-UA-Contact-ID": info.contactID,
"X-UA-Device-Family": "ios",
],
method: "POST",
auth: .contactAuthToken(identifier: info.contactID),
body: try encoder.encode(body)
)

AirshipLogger.trace("Performing additional audience check with request \(request), body: \(body)")

return try await session.performHTTPRequest(request) { data, response in
AirshipLogger.debug("Additional audience check response finished with response: \(response)")

guard (200..<300).contains(response.statusCode) else {
return nil
}

guard let data = data else {
throw AirshipErrors.error("Invalid response body \(String(describing: data))")
}

return try JSONDecoder().decode(AdditionalAudienceCheckResult.self, from: data)
}
}

fileprivate struct RequestBody: Encodable {
let channelID: String
let contactID: String
let namedUserID: String?
let context: AirshipJSON?

enum CodingKeys: String, CodingKey {
case channelID = "channel_id"
case contactID = "contact_id"
case namedUserID = "named_user_id"
case context
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/* Copyright Airship and Contributors */

import Foundation
#if canImport(AirshipCore)
import AirshipCore
#endif

protocol AdditionalAudienceCheckerResolverProtocol: AnyActor {
func resolve(
deviceInfoProvider: AudienceDeviceInfoProvider,
additionalAudienceCheckOverrides: AdditionalAudienceCheckOverrides?
) async throws -> Bool
}

actor AdditionalAudienceCheckerResolver: AdditionalAudienceCheckerResolverProtocol {
private let cache: AirshipCache
private let apiClient: AdditionalAudienceCheckerAPIClientProtocol

private let date: AirshipDateProtocol
private var inProgress: Task<Bool, Error>?
private let configProvider: () -> RemoteConfig.AdditionalAudienceCheckConfig?

private var additionalAudienceConfig: RemoteConfig.AdditionalAudienceCheckConfig? {
get {
configProvider()
}
}

init(
config: RuntimeConfig,
cache: AirshipCache,
date: AirshipDateProtocol = AirshipDate.shared
) {
self.cache = cache
self.apiClient = AdditionalAudienceCheckerAPIClient(config: config)
self.date = date
self.configProvider = {
config.remoteConfig.iaaConfig?.additionalAudienceConfig
}
}

/// Testing
init(
cache: AirshipCache,
apiClient: AdditionalAudienceCheckerAPIClientProtocol,
date: AirshipDateProtocol,
configProvider: @escaping () -> RemoteConfig.AdditionalAudienceCheckConfig?
) {
self.cache = cache
self.apiClient = apiClient
self.date = date
self.configProvider = configProvider
}

func resolve(
deviceInfoProvider: AudienceDeviceInfoProvider,
additionalAudienceCheckOverrides: AdditionalAudienceCheckOverrides?
) async throws -> Bool {

guard
let config = additionalAudienceConfig,
config.isEnabled
else {
return true
}

guard
let urlString = additionalAudienceCheckOverrides?.url ?? config.url,
let url = URL(string: urlString)
else {
AirshipLogger.warn("Failed to parse additional audience check url " +
String(describing: additionalAudienceCheckOverrides) + ", " +
String(describing: config) + ")")
throw AirshipErrors.error("Missing additional audience check url")
}

guard additionalAudienceCheckOverrides?.bypass != true else {
AirshipLogger.trace("Additional audience check is bypassed")
return true
}
let context = additionalAudienceCheckOverrides?.context ?? config.context

_ = try? await inProgress?.value
let task = Task {
return try await doResolve(
url: url,
context: context,
deviceInfoProvider: deviceInfoProvider
)
}

inProgress = task
return try await task.value
}

private func doResolve(
url: URL,
context: AirshipJSON?,
deviceInfoProvider: AudienceDeviceInfoProvider
) async throws -> Bool {

let channelID = try await deviceInfoProvider.channelID
let contactInfo = await deviceInfoProvider.stableContactInfo

let cacheKey = try cacheKey(
url: url.absoluteString,
context: context ?? .null,
contactID: contactInfo.contactID,
channelID: channelID
)

if let cached: AdditionalAudienceCheckResult = await cache.getCachedValue(key: cacheKey) {
return cached.isMatched
}

let request = AdditionalAudienceCheckResult.Request(
url: url,
channelID: channelID,
contactID: contactInfo.contactID,
namedUserID: contactInfo.namedUserID,
context: context
)

let response = try await apiClient.resolve(info: request)

if response.isSuccess, let result = response.result {
await cache.setCachedValue(result, key: cacheKey, ttl: result.cacheTTL)
return result.isMatched
} else if response.isServerError {
throw AirshipErrors.error("Failed to perform additional check due to server error \(response)")
} else {
return false
}
}

private func cacheKey(url: String, context: AirshipJSON, contactID: String, channelID: String) throws -> String {
return String([url, try context.toString(), contactID, channelID].joined(separator: ":"))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,14 @@ public struct AutomationAudience: Codable, Sendable, Equatable {
}
}

struct AdditionalAudienceCheckOverrides: Codable, Sendable, Equatable {
let bypass: Bool?
let context: AirshipJSON?
let url: String?

enum CodingKeys: String, CodingKey {
case bypass, context, url
}
}


Loading

0 comments on commit 770b9c1

Please sign in to comment.