Skip to content

Commit

Permalink
Merge pull request #8 from Topsort/banner.swift
Browse files Browse the repository at this point in the history
feat(banners): add banner swift component
  • Loading branch information
smargozzini authored Aug 8, 2024
2 parents 3f20a4f + 70bd1a6 commit 1e3c2fa
Show file tree
Hide file tree
Showing 8 changed files with 279 additions and 3 deletions.
15 changes: 13 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,19 @@ import PackageDescription
let package = Package(
name: "Topsort-Analytics",
platforms: [
.macOS("10.15"),
.iOS("13.0"),
.macOS("12.00"),
.iOS("15.0"),
.tvOS("11.0"),
.watchOS("7.1")],
products: [
// Products define the executables and libraries a package produces, making them visible to other packages.
.library(
name: "Topsort-Analytics",
targets: ["Topsort-Analytics"]),
.library(
name: "TopsortBanners",
targets: ["TopsortBanners"]
)
],
targets: [
// Targets are the basic building blocks of a package, defining a module or a test suite.
Expand All @@ -24,5 +28,12 @@ let package = Package(
.testTarget(
name: "analytics.swiftTests",
dependencies: ["Topsort-Analytics"]),
.target(
name: "TopsortBanners",
dependencies: ["Topsort-Analytics"]),
.testTarget(
name: "banners.swiftTests",
dependencies: ["TopsortBanners", "Topsort-Analytics"]),

]
)
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,3 +117,26 @@ struct ContentView: View {
}
}
```

### Banners

```swift
import TopsortBanners

struct ContentView: View {
var body: some View {
TopsortBanner(
apiKey: "API_KEY",
url: "https://api.topsort.com/v2",
width: widht,
height: height,
slotId: "slotId",
deviceType: "device"
) { response in
// function to execute when banner is clicked
}
}
}
```

This code will display a banner, send an impression event when the banner is shown, and send a click event when the banner is clicked. Inside the callback, you can add logic to execute when the banner is clicked, such as redirecting to the product page.
14 changes: 13 additions & 1 deletion Sources/Topsort-Analytics/Analytics.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
import Foundation

public class Analytics {

public protocol AnalyticsProtocol {
var opaqueUserId: String { get }
func set(opaqueUserId: String?)
func configure(apiKey: String, url: String?)
func track(impression event: Event)
func track(click event: Event)
func track(purchase event: PurchaseEvent)
func executeAuctions(auctions: [Auction]) async -> AuctionResponse?
}


public class Analytics: AnalyticsProtocol {
public static let shared = Analytics()
@FilePersistedValue(storePath: PathHelper.path(for: "com.topsort.analytics.opaque-user-id.plist"))
private var _opaqueUserId: String?
Expand Down
74 changes: 74 additions & 0 deletions Sources/TopsortBanners/BannerView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import Foundation
import SwiftUI
import Topsort_Analytics

public struct TopsortBanner: View {
@ObservedObject var sharedValues = SharedValues()

var width: CGFloat
var height: CGFloat
var buttonClickedAction: (AuctionResponse?) -> Void
var analytics: AnalyticsProtocol

public init(
apiKey: String,
url: String,
width: CGFloat,
height: CGFloat,
slotId: String,
deviceType: String,
buttonClickedAction: @escaping (AuctionResponse?) -> Void,
analytics: AnalyticsProtocol = Analytics.shared
) {
Analytics.shared.configure(apiKey: apiKey, url: url)
self.width = width
self.height = height
self.buttonClickedAction = buttonClickedAction
self.analytics = analytics

self.run(deviceType: deviceType, slotId: slotId)
}

private func run(deviceType: String, slotId: String) {
Task {
await self.executeAuctions(deviceType: deviceType, slotId: slotId)
}
}

internal func executeAuctions(deviceType: String, slotId: String) async {
let auction: Auction = Auction(type: "banners", slots: 1, slotId: slotId, device: deviceType)
let response = await analytics.executeAuctions(auctions: [auction])

sharedValues.response = response
sharedValues.setResolvedBidIdAndUrlFromResponse()
sharedValues.loading = false

let event = Event(resolvedBidId: sharedValues.resolvedBidId!, occurredAt: Date.now)
analytics.track(impression: event)
}

private func buttonClicked() async {
let event = Event(resolvedBidId: sharedValues.resolvedBidId!, occurredAt: Date.now)
analytics.track(click: event)
self.buttonClickedAction(sharedValues.response)
}

public var body: some View {
VStack {
if sharedValues.loading {
ProgressView()
} else {
Button(action: {
Task {
await self.buttonClicked()
}
}) {
AsyncImage(url: URL(string: sharedValues.urlString!))
.frame(width: self.width, height: self.height)
.clipped()
}
.frame(width: self.width, height: self.height)
}
}
}
}
35 changes: 35 additions & 0 deletions Sources/TopsortBanners/SharedValues.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import Foundation
import Topsort_Analytics

class SharedValues: ObservableObject {
@Published var resolvedBidId: String?
@Published var loading: Bool
@Published var urlString: String?
@Published var response: AuctionResponse?

init() {
resolvedBidId = nil
loading = true
urlString = nil
response = nil
}

public func setResolvedBidIdAndUrlFromResponse() {
guard let response = self.response else {
return
}
let results = response.results

guard let winners = results.first?.winners else {
return
}
guard let winner = winners.first else {
return
}
guard let asset = winner.asset?.first else {
return
}
resolvedBidId = winner.resolvedBidId
urlString = asset.url
}
}
60 changes: 60 additions & 0 deletions Tests/banners.swiftTests/bannerView_swiftTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import XCTest
@testable import TopsortBanners
@testable import Topsort_Analytics

class TopsortBannerTests: XCTestCase {

func testTopsortBannerInitialization() {
let expectation = self.expectation(description: "Button clicked action")
let banner = TopsortBanner(
apiKey: "test_api_key",
url: "test_url",
width: 300,
height: 250,
slotId: "test_slot_id",
deviceType: "test_device_type"
) { response in
expectation.fulfill()
}

XCTAssertEqual(banner.width, 300)
XCTAssertEqual(banner.height, 250)

banner.buttonClickedAction(nil)

wait(for: [expectation], timeout: 1.0)
}

func testExecuteAuctions() async {
// Mock the response
let asset = Asset(url: "https://example.com")
let winner = Winner(rank: 1, asset: [asset], type: "type", id: "id", resolvedBidId: "resolved_bid_id")
let auctionResult = AuctionResult(resultType: "result_type", winners: [winner], error: false)
let auctionResponse = AuctionResponse(results: [auctionResult])

// Mock Analytics and response
let mockAnalytics = MockAnalytics()
mockAnalytics.executeAuctionsMockResponse = auctionResponse

let banner = await TopsortBanner(
apiKey: "test_api_key",
url: "test_url",
width: 300,
height: 250,
slotId: "test_slot_id",
deviceType: "test_device_type",
buttonClickedAction: { response in
},
analytics: mockAnalytics
)

// Execute the method
await banner.executeAuctions(deviceType: "test_device_type", slotId: "test_slot_id")

await MainActor.run {
XCTAssertEqual(banner.sharedValues.resolvedBidId, "resolved_bid_id")
XCTAssertEqual(banner.sharedValues.urlString, "https://example.com")
XCTAssertFalse(banner.sharedValues.loading)
}
}
}
32 changes: 32 additions & 0 deletions Tests/banners.swiftTests/mocks/analyticsMock.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import Foundation
import XCTest
@testable import Topsort_Analytics

public class MockAnalytics: AnalyticsProtocol {
public var opaqueUserId: String = "mocked-opaque-user-id"
public var executeAuctionsMockResponse: AuctionResponse?

public func set(opaqueUserId: String?) {
// Mock implementation
}

public func configure(apiKey: String, url: String? = nil) {
// Mock implementation
}

public func track(impression event: Event) {
// Mock implementation
}

public func track(click event: Event) {
// Mock implementation
}

public func track(purchase event: PurchaseEvent) {
// Mock implementation
}

public func executeAuctions(auctions: [Auction]) async -> AuctionResponse? {
return executeAuctionsMockResponse
}
}
29 changes: 29 additions & 0 deletions Tests/banners.swiftTests/sharedValues_swiftTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import XCTest
@testable import TopsortBanners
@testable import Topsort_Analytics

class SharedValuesTests: XCTestCase {

func testSharedValuesInitialization() {
let sharedValues = SharedValues()

XCTAssertNil(sharedValues.resolvedBidId)
XCTAssertTrue(sharedValues.loading)
XCTAssertNil(sharedValues.urlString)
XCTAssertNil(sharedValues.response)
}

func testSetResolvedBidIdAndUrlFromResponse() {
let asset = Asset(url: "https://example.com")
let winner = Winner(rank: 1, asset: [asset], type: "type", id: "id", resolvedBidId: "resolved_bid_id")
let auctionResult = AuctionResult(resultType: "result_type", winners: [winner], error: false)
let auctionResponse = AuctionResponse(results: [auctionResult])

let sharedValues = SharedValues()
sharedValues.response = auctionResponse
sharedValues.setResolvedBidIdAndUrlFromResponse()

XCTAssertEqual(sharedValues.resolvedBidId, "resolved_bid_id")
XCTAssertEqual(sharedValues.urlString, "https://example.com")
}
}

0 comments on commit 1e3c2fa

Please sign in to comment.