import SwiftUI
import Foundation
// 1. Initialize the SDK at app launch
@main
struct YourApp: App {
init() {
OrganiqSDK.initialize(
appId: "YOUR_APP_ID",
apiKey: "YOUR_API_KEY"
)
}
var body: some Scene {
WindowGroup {
ContentView() // Your main app view
}
}
}
// 2. Organiq SDK core — paste this block as-is into your project.
// It calls https://organiq.app/api/attribution directly and scopes every
// request to the appId passed into initialize() above, so this
// exact file already talks to YOUR account's videos and data.
struct VideoCandidate: Identifiable, Decodable {
let id: String
let caption: String?
let thumbnailUrl: String?
private enum CodingKeys: String, CodingKey {
case id, caption, frames
case thumbnailUrl = "thumbnail_url"
}
init(from decoder: Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self)
id = try c.decode(String.self, forKey: .id)
caption = try? c.decodeIfPresent(String.self, forKey: .caption)
if let frames = try? c.decodeIfPresent([String].self, forKey: .frames), let first = frames?.first {
thumbnailUrl = first
} else {
thumbnailUrl = try? c.decodeIfPresent(String.self, forKey: .thumbnailUrl)
}
}
}
final class OrganiqSDK {
static let shared = OrganiqSDK()
private init() {}
private var appId = ""
private var apiKey = ""
private let baseURL = URL(string: "https://organiq.app/api/attribution")!
private lazy var sessionId: String = UUID().uuidString
static func initialize(appId: String, apiKey: String) {
shared.appId = appId
shared.apiKey = apiKey
}
static func trackAppLaunch(platform: String) {
shared.send(action: "track-session-start", extra: [
"session_id": shared.sessionId,
"platform": platform
])
}
static func getVelocityCandidates(source: String, completion: @escaping ([VideoCandidate]) -> Void) {
shared.send(action: "get-candidates", extra: ["source": source]) { json in
guard let data = json?["data"] as? [String: Any],
let top4 = data["top4"] as? [[String: Any]] else {
completion([])
return
}
let decoder = JSONDecoder()
let candidates = top4.compactMap { dict -> VideoCandidate? in
guard let raw = try? JSONSerialization.data(withJSONObject: dict) else { return nil }
return try? decoder.decode(VideoCandidate.self, from: raw)
}
completion(candidates)
}
}
static func recordAttribution(videoId: String, source: String) {
shared.send(action: "save", extra: [
"source": source,
"description": "Matched via Velocity Survey",
"matched_video_id": videoId,
"confidence": 98
])
}
static func recordChannelAttribution(channel: String) {
shared.send(action: "save", extra: [
"source": channel,
"description": "Direct channel selection: \(channel)",
"confidence": 100
])
}
static func trackFunnelStep(
step: String,
durationSeconds: Double,
selectedValue: String?,
matchedVideoId: String? = nil,
finalSource: String? = nil
) {
var extra: [String: Any] = [
"session_id": shared.sessionId,
"step_name": step,
"duration_ms": Int(durationSeconds * 1000)
]
if let selectedValue = selectedValue {
extra["selected_value"] = selectedValue
}
if let matchedVideoId = matchedVideoId {
extra["matched_video_id"] = matchedVideoId
}
if let finalSource = finalSource {
extra["final_source"] = finalSource
}
shared.send(action: "track-funnel-step", extra: extra)
}
// MARK: - Networking
private func send(action: String, extra: [String: Any], completion: (([String: Any]?) -> Void)? = nil) {
var payload = extra
payload["action"] = action
payload["app_id"] = appId
var request = URLRequest(url: baseURL)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.setValue(apiKey, forHTTPHeaderField: "x-api-key")
request.setValue(appId, forHTTPHeaderField: "x-app-id")
request.httpBody = try? JSONSerialization.data(withJSONObject: payload)
URLSession.shared.dataTask(with: request) { data, _, _ in
let json = data.flatMap { try? JSONSerialization.jsonObject(with: $0) as? [String: Any] }
DispatchQueue.main.async {
completion?(json)
}
}.resume()
}
}
// 3. Add the Survey View to your onboarding flow
struct OrganiqSurveyView: View {
@Binding var isPresented: Bool
@State private var step: String = "source"
@State private var source: String = ""
@State private var top4Videos: [VideoCandidate] = []
@State private var selectedVideo: VideoCandidate? = nil
@State private var isLoading: Bool = false
// Adapt these colors/fonts to match your app's design system
let primaryColor = Color.primary
let backgroundColor = Color(uiColor: .systemBackground)
let cardBackgroundColor = Color(uiColor: .secondarySystemBackground)
let accentColor = Color.accentColor
var body: some View {
ZStack {
backgroundColor.ignoresSafeArea()
VStack(spacing: 24) {
if step == "source" {
sourceView
} else if step == "top4" {
top4View
}
}
.padding(24)
.onAppear {
self.screenStartTime = Date()
OrganiqSDK.trackAppLaunch(platform: "iOS")
}
}
}
// MARK: - Views
private var sourceView: some View {
VStack(spacing: 20) {
Text("Where did you discover us?")
.font(.system(size: 28, weight: .bold, design: .default))
.foregroundColor(primaryColor)
.multilineTextAlignment(.center)
.padding(.bottom, 8)
VStack(spacing: 12) {
SocialButton(title: "TikTok", icon: "play.tv.fill") {
self.source = "TikTok"
self.fetchVelocityCandidates()
}
SocialButton(title: "Other social network", icon: "square.and.arrow.up") {
self.source = "OtherSocial"
self.fetchVelocityCandidates()
}
SocialButton(title: "App Store", icon: "magnifyingglass", isSecondary: true) {
self.recordDirectAttribution(channel: "AppStore")
}
SocialButton(title: "Friend / Recommendation", icon: "person.2.fill", isSecondary: true) {
self.recordDirectAttribution(channel: "Friend")
}
SocialButton(title: "Other channel", icon: "globe", isSecondary: true) {
self.recordDirectAttribution(channel: "Other")
}
}
Spacer()
}
}
private var top4View: some View {
VStack(spacing: 20) {
Text("Did you see one of these videos?")
.font(.system(size: 24, weight: .bold, design: .default))
.foregroundColor(primaryColor)
.multilineTextAlignment(.center)
.padding(.bottom, 8)
if isLoading {
Spacer()
ProgressView()
.scaleEffect(1.5)
Spacer()
} else {
LazyVGrid(columns: [GridItem(.flexible(), spacing: 16), GridItem(.flexible(), spacing: 16)], spacing: 16) {
ForEach(top4Videos) { video in
VideoCard(video: video, isSelected: selectedVideo?.id == video.id) {
self.selectedVideo = video
// Give UI time to show selection state
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
let dwellSec = Date().timeIntervalSince(self.screenStartTime)
OrganiqSDK.trackFunnelStep(step: "top4", durationSeconds: dwellSec, selectedValue: video.id)
OrganiqSDK.recordAttribution(videoId: video.id, source: self.source)
OrganiqSDK.trackFunnelStep(
step: "success",
durationSeconds: 0.5,
selectedValue: video.id,
matchedVideoId: video.id,
finalSource: self.source
)
self.isPresented = false
}
}
}
}
Spacer()
Button(action: {
let dwellSec = Date().timeIntervalSince(self.screenStartTime)
OrganiqSDK.trackFunnelStep(step: "top4", durationSeconds: dwellSec, selectedValue: "none_of_these_unattributed")
OrganiqSDK.trackFunnelStep(step: "success", durationSeconds: 0.3, selectedValue: "none_of_these_unattributed")
self.isPresented = false
}) {
Text("None of these videos")
.font(.system(size: 15, weight: .semibold))
.foregroundColor(.secondary)
.frame(maxWidth: .infinity)
.padding()
.background(cardBackgroundColor)
.cornerRadius(16)
}
}
}
}
// MARK: - Logic
@State private var screenStartTime: Date = Date()
private func trackTransition(from: String, to: String, value: String? = nil) {
let dwellSec = Date().timeIntervalSince(screenStartTime)
OrganiqSDK.trackFunnelStep(step: from, durationSeconds: dwellSec, selectedValue: value)
self.screenStartTime = Date()
self.step = to
}
private func fetchVelocityCandidates() {
self.isLoading = true
self.trackTransition(from: "source", to: "top4", value: self.source)
OrganiqSDK.getVelocityCandidates(source: self.source) { candidates in
self.top4Videos = Array(candidates.prefix(4))
self.isLoading = false
}
}
private func recordDirectAttribution(channel: String) {
let dwellSec = Date().timeIntervalSince(screenStartTime)
OrganiqSDK.trackFunnelStep(step: "source", durationSeconds: dwellSec, selectedValue: channel)
OrganiqSDK.recordChannelAttribution(channel: channel)
OrganiqSDK.trackFunnelStep(step: "success", durationSeconds: 0.4, selectedValue: channel, finalSource: channel)
self.isPresented = false
}
}
// MARK: - Components
struct SocialButton: View {
let title: String
let icon: String
var isSecondary: Bool = false
let action: () -> Void
var body: some View {
Button(action: action) {
HStack(spacing: 12) {
Image(systemName: icon)
.font(.system(size: 18, weight: .semibold))
Text(title)
.font(.system(size: 16, weight: .bold))
Spacer()
Image(systemName: "chevron.right")
.font(.system(size: 14, weight: .bold))
.opacity(0.3)
}
.foregroundColor(isSecondary ? .primary : .white)
.padding(.horizontal, 20)
.padding(.vertical, 16)
.background(isSecondary ? Color(uiColor: .secondarySystemBackground) : Color.accentColor)
.cornerRadius(16)
}
}
}
struct VideoCard: View {
let video: VideoCandidate
let isSelected: Bool
let action: () -> Void
var body: some View {
Button(action: action) {
ZStack(alignment: .bottomLeading) {
// Background Image
AsyncImage(url: URL(string: video.thumbnailUrl ?? "")) { phase in
if let image = phase.image {
image
.resizable()
.aspectRatio(contentMode: .fill)
} else {
Color(uiColor: .secondarySystemBackground)
.overlay(
Image(systemName: "play.circle.fill")
.font(.system(size: 30))
.foregroundColor(.secondary.opacity(0.5))
)
}
}
// Gradient Overlay
LinearGradient(
gradient: Gradient(colors: [.black.opacity(0.8), .clear]),
startPoint: .bottom,
endPoint: .top
)
// Caption
Text(video.caption ?? "Organic Video")
.font(.system(size: 11, weight: .semibold))
.foregroundColor(.white)
.lineLimit(2)
.multilineTextAlignment(.leading)
.padding(12)
// Selection Border
if isSelected {
RoundedRectangle(cornerRadius: 16)
.stroke(Color.accentColor, lineWidth: 4)
}
}
.frame(maxWidth: .infinity)
.aspectRatio(9/14, contentMode: .fit)
.cornerRadius(16)
.clipped()
// Scale animation on press
.scaleEffect(isSelected ? 0.95 : 1.0)
.animation(.spring(response: 0.3, dampingFraction: 0.6), value: isSelected)
}
.buttonStyle(.plain)
}
}