From 96866dcaf6f2ace2a86b3b2c8d2d6be1686d093c Mon Sep 17 00:00:00 2001 From: saravanakumardb1 Date: Thu, 19 Mar 2026 14:22:18 -0700 Subject: [PATCH] fix(swift-sdk): add buildRequest, BLPlatformError, fix SDK compile errors for iOS 26 --- .../Sources/BLBroadcastClient.swift | 3 +- .../Sources/BLDeepLinkRouter.swift | 1 + .../Sources/BLInAppMessageUI.swift | 69 ++++++------------ .../Sources/BLPlatformClient.swift | 40 +++++++++++ .../Sources/BLSurveyClient.swift | 3 +- .../Sources/BLSurveyUI.swift | 72 ++++++++++--------- 6 files changed, 107 insertions(+), 81 deletions(-) diff --git a/packages/swift-platform-sdk/Sources/BLBroadcastClient.swift b/packages/swift-platform-sdk/Sources/BLBroadcastClient.swift index 22899536..888e9d7f 100644 --- a/packages/swift-platform-sdk/Sources/BLBroadcastClient.swift +++ b/packages/swift-platform-sdk/Sources/BLBroadcastClient.swift @@ -48,7 +48,8 @@ public struct BLInAppMessage: Codable, Sendable, Identifiable { } /// Broadcast client for fetching and managing in-app messages. -public actor BLBroadcastClient { +@available(iOS 15.0, macOS 12.0, watchOS 8.0, *) +public class BLBroadcastClient: ObservableObject { private let platformClient: BLPlatformClient private var pollTask: Task? diff --git a/packages/swift-platform-sdk/Sources/BLDeepLinkRouter.swift b/packages/swift-platform-sdk/Sources/BLDeepLinkRouter.swift index 31ee8084..96468bf8 100644 --- a/packages/swift-platform-sdk/Sources/BLDeepLinkRouter.swift +++ b/packages/swift-platform-sdk/Sources/BLDeepLinkRouter.swift @@ -1,4 +1,5 @@ import Foundation +import os /** * Deep Link Router — Swift diff --git a/packages/swift-platform-sdk/Sources/BLInAppMessageUI.swift b/packages/swift-platform-sdk/Sources/BLInAppMessageUI.swift index afd3166a..3cdf1553 100644 --- a/packages/swift-platform-sdk/Sources/BLInAppMessageUI.swift +++ b/packages/swift-platform-sdk/Sources/BLInAppMessageUI.swift @@ -40,33 +40,34 @@ public struct BLInAppMessageBanner: View { } private func loadMessages() async { - let result = await client.listMessages() - if let response = result.1 { - messages = response.messages + do { + messages = try await client.listMessages() unreadCount = messages.filter { $0.status == .unread }.count + } catch { + // Silently ignore load errors } } private func startPolling() { - client.startPolling(intervalMs: 60000) { updatedMessages in - messages = updatedMessages - unreadCount = updatedMessages.filter { $0.status == .unread }.count + client.startPolling(interval: 60) { updatedMessages in + self.messages = updatedMessages + self.unreadCount = updatedMessages.filter { $0.status == .unread }.count } } private func dismissMessage(_ message: BLInAppMessage) async { - _ = await client.markDismissed(message.id) + try? await client.markDismissed(messageId: message.id) messages.removeAll { $0.id == message.id } } private func handleTap(_ message: BLInAppMessage) async { - _ = await client.trackClick(message.id) + _ = try? await client.trackClick(messageId: message.id) if let urlString = message.ctaUrl, let url = URL(string: urlString) { await UIApplication.shared.open(url) } - _ = await client.markRead(message.id) + try? await client.markRead(messageId: message.id) if let index = messages.firstIndex(where: { $0.id == message.id }) { - messages[index].status = .read + messages[index] = message // refresh from next poll } } } @@ -79,25 +80,12 @@ struct BannerCard: View { var body: some View { HStack(alignment: .top, spacing: 12) { - if let imageUrl = message.imageUrl, let url = URL(string: imageUrl) { - AsyncImage(url: url) { phase in - switch phase { - case .success(let image): - image.resizable().aspectRatio(contentMode: .fill) - default: - Color.gray - } - } - .frame(width: 48, height: 48) - .clipShape(RoundedRectangle(cornerRadius: 8)) - } - VStack(alignment: .leading, spacing: 4) { Text(message.title) .font(.headline) - if let body = message.body { - Text(body) + if !message.body.isEmpty { + Text(message.body) .font(.subheadline) .foregroundColor(.secondary) .lineLimit(2) @@ -171,20 +159,20 @@ public struct BLBroadcastModal: View { } private func startPolling() { - client.startPolling(intervalMs: 30000) { messages in + client.startPolling(interval: 30) { messages in let modalMessages = messages.filter { $0.status == .unread && ($0.style == .modal || $0.style == .fullscreen) } - if let first = modalMessages.first, currentMessage == nil { - currentMessage = first - isPresented = true + if let first = modalMessages.first, self.currentMessage == nil { + self.currentMessage = first + self.isPresented = true } } } private func dismissMessage() async { if let message = currentMessage { - _ = await client.markDismissed(message.id) + try? await client.markDismissed(messageId: message.id) } isPresented = false currentMessage = nil @@ -192,11 +180,11 @@ public struct BLBroadcastModal: View { private func handleAction() async { if let message = currentMessage { - _ = await client.trackClick(message.id) + _ = try? await client.trackClick(messageId: message.id) if let urlString = message.ctaUrl, let url = URL(string: urlString) { await UIApplication.shared.open(url) } - _ = await client.markRead(message.id) + try? await client.markRead(messageId: message.id) } isPresented = false currentMessage = nil @@ -213,24 +201,11 @@ struct ModalContent: View { NavigationView { ScrollView { VStack(spacing: 20) { - if let imageUrl = message.imageUrl, let url = URL(string: imageUrl) { - AsyncImage(url: url) { phase in - switch phase { - case .success(let image): - image.resizable().aspectRatio(contentMode: .fit) - default: - Color.gray - } - } - .frame(maxHeight: 200) - .clipShape(RoundedRectangle(cornerRadius: 12)) - } - Text(message.title) .font(.title2.bold()) - if let body = message.body { - Text(body) + if !message.body.isEmpty { + Text(message.body) .font(.body) .foregroundColor(.secondary) .multilineTextAlignment(.center) diff --git a/packages/swift-platform-sdk/Sources/BLPlatformClient.swift b/packages/swift-platform-sdk/Sources/BLPlatformClient.swift index cb275f00..8017f835 100644 --- a/packages/swift-platform-sdk/Sources/BLPlatformClient.swift +++ b/packages/swift-platform-sdk/Sources/BLPlatformClient.swift @@ -164,6 +164,35 @@ public final class BLPlatformClient: @unchecked Sendable { session.dataTask(with: request) { _, _, _ in }.resume() } + // MARK: - Request Builder + + /// Build an authenticated URLRequest (used by BLBroadcastClient, BLSurveyClient). + public func buildRequest( + path: String, + method: String = "GET", + body: [String: Any]? = nil + ) throws -> URLRequest { + guard let url = URL(string: "\(config.baseURL)\(path)") else { + throw BLNetworkError.invalidURL(path) + } + + var request = URLRequest(url: url) + request.httpMethod = method + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.setValue(config.productId, forHTTPHeaderField: "X-Product-Id") + request.setValue(UUID().uuidString, forHTTPHeaderField: "X-Request-Id") + + if let token = authToken { + request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization") + } + + if let body { + request.httpBody = try JSONSerialization.data(withJSONObject: body) + } + + return request + } + // MARK: - Encoder/Decoder Access public var jsonEncoder: JSONEncoder { encoder } @@ -172,6 +201,17 @@ public final class BLPlatformClient: @unchecked Sendable { // MARK: - Errors +/// Legacy alias used by BLBroadcastClient, BLSurveyClient, BLDeepLinkRouter. +public enum BLPlatformError: LocalizedError { + case requestFailed(String) + + public var errorDescription: String? { + switch self { + case .requestFailed(let msg): return msg + } + } +} + public enum BLNetworkError: LocalizedError { case invalidURL(String) case invalidResponse diff --git a/packages/swift-platform-sdk/Sources/BLSurveyClient.swift b/packages/swift-platform-sdk/Sources/BLSurveyClient.swift index 89952687..dc31f23c 100644 --- a/packages/swift-platform-sdk/Sources/BLSurveyClient.swift +++ b/packages/swift-platform-sdk/Sources/BLSurveyClient.swift @@ -108,7 +108,8 @@ public struct BLSurveyResponse: Codable, Sendable { } /// Survey client for managing in-app surveys. -public actor BLSurveyClient { +@available(iOS 15.0, macOS 12.0, watchOS 8.0, *) +public class BLSurveyClient: ObservableObject { private let platformClient: BLPlatformClient private var pollTask: Task? private var cachedResponses: [String: BLSurveyResponse] = [:] diff --git a/packages/swift-platform-sdk/Sources/BLSurveyUI.swift b/packages/swift-platform-sdk/Sources/BLSurveyUI.swift index b3cc159c..57fb8875 100644 --- a/packages/swift-platform-sdk/Sources/BLSurveyUI.swift +++ b/packages/swift-platform-sdk/Sources/BLSurveyUI.swift @@ -63,44 +63,49 @@ public struct BLSurveyModal: View { } private func checkForSurvey() async { - let result = await client.getActiveSurvey() - if let activeSurvey = result.1 { - survey = activeSurvey - if !isPresented { - isPresented = true + do { + if let activeSurvey = try await client.getActiveSurvey() { + survey = activeSurvey + if !isPresented { + isPresented = true + } } + } catch { + // Silently ignore } } private func startPolling() { - client.startPolling(intervalMs: 60000) { newSurvey in - if let newSurvey = newSurvey, survey == nil { - survey = newSurvey - isPresented = true + client.startPolling(interval: 60) { newSurvey in + if let newSurvey = newSurvey, self.survey == nil { + self.survey = newSurvey + self.isPresented = true } } } - private func submitAnswer(_ question: BLQuestion) async { + private func submitAnswer(_ question: BLSurveyQuestion) async { let answer: BLSurveyAnswer switch question.type { case .singleChoice, .dropdown: guard let value = selectedOption else { return } - answer = BLSurveyAnswer(type: "single_choice", value: .string(value)) + answer = .singleChoice(optionId: value) case .multipleChoice: let values = Array(selectedOptions) - answer = BLSurveyAnswer(type: "multiple_choice", value: .stringArray(values)) - case .rating, .scale, .nps: - answer = BLSurveyAnswer(type: "rating", value: .int(ratingValue)) + answer = .multipleChoice(optionIds: values) + case .rating, .scale: + answer = .rating(value: ratingValue) + case .nps: + answer = .nps(value: ratingValue) case .textShort, .textLong: - answer = BLSurveyAnswer(type: "text", value: .string(textAnswer)) + answer = .text(value: textAnswer) case .ranking: - answer = BLSurveyAnswer(type: "ranking", value: .stringArray(rankingOrder)) + answer = .ranking(rankedOptionIds: rankingOrder) } - let result = await client.submitAnswer(surveyId: survey!.id, questionId: question.id, answer: answer) - if let response = result.1 { + do { + let response = try await client.submitAnswer(surveyId: survey!.id, questionId: question.id, answer: answer) currentQuestionIndex = response.currentQuestionIndex answers = response.answers resetQuestionState() @@ -108,16 +113,15 @@ public struct BLSurveyModal: View { if currentQuestionIndex >= survey!.questions.count { await completeSurvey() } + } catch { + // Silently ignore submit errors } } private func skipQuestion() async { let question = survey!.questions[currentQuestionIndex] if !question.required { - // Submit empty answer to skip - let answer = BLSurveyAnswer(type: "skipped", value: .null) - _ = await client.submitAnswer(surveyId: survey!.id, questionId: question.id, answer: answer) - + // Skip by advancing without submitting currentQuestionIndex += 1 resetQuestionState() @@ -128,17 +132,21 @@ public struct BLSurveyModal: View { } private func completeSurvey() async { - let result = await client.completeSurvey(surveyId: survey!.id) - if let completion = result.1, completion.success { - isComplete = true - showCompletion = true + do { + let completion = try await client.completeSurvey(surveyId: survey!.id) + if completion.success { + isComplete = true + showCompletion = true + } + } catch { + // Silently ignore } } private func dismissSurvey() { if let survey = survey { Task { - _ = await client.dismissSurvey(surveyId: survey.id) + try? await client.dismissSurvey(surveyId: survey.id) } } isPresented = false @@ -166,7 +174,7 @@ public struct BLSurveyModal: View { @available(iOS 15.0, *) struct QuestionView: View { let survey: BLActiveSurvey - let question: BLQuestion + let question: BLSurveyQuestion let questionIndex: Int let totalQuestions: Int @@ -314,7 +322,7 @@ struct QuestionView: View { @available(iOS 15.0, *) struct SingleChoiceView: View { - let options: [BLQuestionOption] + let options: [BLSurveyOption] @Binding var selected: String? var body: some View { @@ -348,7 +356,7 @@ struct SingleChoiceView: View { @available(iOS 15.0, *) struct MultipleChoiceView: View { - let options: [BLQuestionOption] + let options: [BLSurveyOption] @Binding var selected: Set var body: some View { @@ -459,7 +467,7 @@ struct TextAnswerView: View { @available(iOS 15.0, *) struct RankingView: View { - let options: [BLQuestionOption] + let options: [BLSurveyOption] @Binding var order: [String] var body: some View { @@ -555,7 +563,7 @@ struct CompletionView: View { HStack { Image(systemName: "gift.fill") .foregroundColor(.green) - Text("You've earned \(incentive.amount) \(incentive.type == .proDays ? "Pro Days" : "Credits")!") + Text("You've earned \(incentive.amount) \(incentive.type == "pro_days" ? "Pro Days" : "Credits")!") .font(.headline) .foregroundColor(.green) }