diff --git a/README.md b/README.md index 7afd8d0..f561804 100644 --- a/README.md +++ b/README.md @@ -4,16 +4,17 @@ This project helps developers understand how to: -- Set up and use `ShortIOSDK` - Generate short URLs with customizable parameters -- Integrate and handle Universal Links in SwiftUI and UIKit +- Handle deep links via Universal Links +- Track conversions +- Use secure (encrypted) short links ## ๐Ÿ“ฆ Requirements - iOS 13.0+ - Xcode 13.0+ - Swift 5+ -- A valid [Short.io](https://short.io/) account +- A valid `enterprises` [Short.io](https://short.io/) account ## ๐Ÿš€ Getting Started @@ -35,24 +36,37 @@ Open `ShortIOApp.xcodeproj` or `ShortIOApp.xcworkspace` in Xcode, depending on t ## ๐Ÿ›  Setup Instructions -### ๐Ÿ”‘ 1. Add Your API Key +### Initialize the SDK -Open the appropriate file: +Before using any functionality, you must initialize the SDK using your API key and domain in `AppDelegate` as part of application(launchOptions) for a UIKit app, or the @main initialization logic for a SwiftUI app. -- **SwiftUI:** `ContentView.swift` -- **UIKit:** `ViewController.swift` -Replace the placeholder with your **Short.io Public API Key:** -```bash -let apiKey = "your_api_key" + +```swift +... +import ShortIOSDK +... + +class AppDelegate: UIResponder, UIApplicationDelegate { + ... + func application(...) { + ... + let sdk = ShortIOSDK.shared + + sdk.initialize(apiKey: "your_apiKey_here", domain: "your_domain_here") + ... + } + ... +} ``` +**Note:** Both `apiKey` and `domain` are the required parameters. + ๐Ÿ”— **Need help finding your API key?** Follow this guide in the [ShortIOSDK README](https://github.com/Short-io/ios-sdk?tab=readme-ov-file#step-1-get-public-api-key-from-shortio). - ### ๐ŸŒ 2. Set Short Link Parameters In the same file (`ContentView.swift` or `ViewController.swift`), provide your **Short.io domain** and the **original URL** you want to shorten: @@ -74,11 +88,11 @@ The app demonstrates: Using your domain and original URL, you can generate a short link like this: ```swift -let sdk = ShortIOSDK() +let sdk = ShortIOSDK.shared let parameters = ShortIOParameters( domain: "your_domain", - originalURL: "https://yourdomain.com" + originalURL: "https://{your_domain}" ) let apiKey = "your_api_key" @@ -98,6 +112,58 @@ Task { } ``` +**โš ๏ธ Note**: Both `apiKey` and `domain` parameters is deprecated. Use the instance's configured API key instead. Call initialize(apiKey:domain:) before using this method + +### ๐Ÿ” Secure Short Links (Encrypted) + +If you want to encrypt the original URL, the SDK provides a `createSecure` function that uses AES-GCM encryption. + +#### ๐Ÿ”ง Example + +```swift +let sdk = ShortIOSDK.shared + +Task { + do { + let result = try sdk.createSecure(originalURL: "your_originalURL_here") + print("result", result.securedOriginalURL, result.securedShortUrl) + } catch { + print("Failed to create secure URL: \(error)") + } +} +``` +#### ๐Ÿงพ Output Format + +- **`securedOriginalURL:`** An encrypted URL like `shortsecure://?` + +- **`securedShortUrl:`** A Base64-encoded decryption key to be appended as a fragment (e.g. `#`) + +### ๐Ÿ”„ Conversion Tracking + +Track conversions for your short links to measure campaign effectiveness. The SDK provides a simple method to record conversions. + +```swift +import ShortIOSDK + +let sdk = ShortIOSDK.shared + +Task { + do { + let result = try await sdk.trackConversion( + domain: "your_domain", // โš ๏ธ Deprecated (optional): + clid: "your_clid", // โš ๏ธ Deprecated (optional): + conversionId: "your_conversionID" (optional) + ) + print("result", result) + } catch { + print("Failed to track conversion: \(error)") + } +} +``` + +**โš ๏ธ Note:** All three parameters โ€” `domain`, `clid`, and `conversionId` โ€” are optional. +- `domain` and `clid` are deprecated and may be removed in future versions. + ## ๐ŸŒ Handling Universal Links ### SwiftUI Implementation @@ -106,8 +172,20 @@ Use the `.onOpenURL` modifier to process incoming links: ```swift .onOpenURL { url in + print("url", url) sdk.handleOpen(url) { result in - print("Navigated to path: \(result?.path ?? "")") + switch result { + case .success(let result): + // Handle successful URL processing + print( + "Original URL: \(result.url)", + "Host: \(result.host), Path: \(result.path)", + "QueryParams: \(result.queryItems)" + ) + case .failure(let error): + // Handle error with proper error type + print("Error: \(error.localizedDescription)") + } } } ``` @@ -123,8 +201,19 @@ func scene(_ scene: UIScene, continue userActivity: NSUserActivity) { print("Invalid universal link or URL components") return } - sdk.handleOpen(incomingURL) { result in - print("Host: \(result?.host), Path: \(result?.path)") + sdk.handleOpen(incomingURL) { result in + switch result { + case .success(let result): + // Handle successful URL processing + print( + "Original URL: \(result.url)", + "Host: \(result.host), Path: \(result.path)", + "QueryParams: \(result.queryItems)" + ) + case .failure(let error): + // Handle error with proper error type + print("Error: \(error.localizedDescription)") + } } } ``` diff --git a/StoryboardProject/ShortIOApp/AppDelegate.swift b/StoryboardProject/ShortIOApp/AppDelegate.swift index f7f60fd..44bc04e 100644 --- a/StoryboardProject/ShortIOApp/AppDelegate.swift +++ b/StoryboardProject/ShortIOApp/AppDelegate.swift @@ -1,4 +1,5 @@ import UIKit +import ShortIOSDK @main class AppDelegate: UIResponder, UIApplicationDelegate { @@ -6,6 +7,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + + let sdk = ShortIOSDK.shared + + sdk.initialize(apiKey: "your_api_key_here", domain: "your_domain_here") + // Override point for customization after application launch. return true } diff --git a/StoryboardProject/ShortIOApp/SceneDelegate.swift b/StoryboardProject/ShortIOApp/SceneDelegate.swift index 42734ab..5774087 100644 --- a/StoryboardProject/ShortIOApp/SceneDelegate.swift +++ b/StoryboardProject/ShortIOApp/SceneDelegate.swift @@ -4,8 +4,8 @@ import ShortIOSDK class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? - private let sdk = ShortIOSDK() - + private let sdk = ShortIOSDK.shared + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. @@ -21,7 +21,14 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { return } sdk.handleOpen(incomingURL) { result in - print("Host: \(result?.host), Path: \(result?.path)") + switch result { + case .success(let result): + // Handle successful URL processing + print("result", result, "Host: \(result.host), Path: \(result.path)", "QueryParams: \(result.queryItems)") + case .failure(let error): + // Handle error with proper error type + print("Error: \(error.localizedDescription)") + } } } diff --git a/StoryboardProject/ShortIOApp/ViewController.swift b/StoryboardProject/ShortIOApp/ViewController.swift index 2e0365f..4a38ac2 100644 --- a/StoryboardProject/ShortIOApp/ViewController.swift +++ b/StoryboardProject/ShortIOApp/ViewController.swift @@ -3,7 +3,7 @@ import ShortIOSDK class ViewController: UIViewController { - private let shortLinkSDK = ShortIOSDK() + private let shortLinkSDK = ShortIOSDK.shared private let titleLabel: UILabel = { let label = UILabel() @@ -13,7 +13,7 @@ class ViewController: UIViewController { return label }() - private let createButton: UIButton = { + private let createShortLinkButton: UIButton = { let button = UIButton(type: .system) button.setTitle("Create Short Link", for: .normal) button.titleLabel?.font = .systemFont(ofSize: 18, weight: .semibold) @@ -24,9 +24,32 @@ class ViewController: UIViewController { return button }() - private let activityIndicator = UIActivityIndicatorView(style: .medium) + private let createSecureShortLinkButton: UIButton = { + let button = UIButton(type: .system) + button.setTitle("Create Secure Short Link", for: .normal) + button.titleLabel?.font = .systemFont(ofSize: 18, weight: .semibold) + button.backgroundColor = .systemBlue + button.tintColor = .white + button.layer.cornerRadius = 10 + button.addTarget(self, action: #selector(createSecureShortLink), for: .touchUpInside) + return button + }() - private let loadingLabel: UILabel = { + private let conversionTrackingButton: UIButton = { + let button = UIButton(type: .system) + button.setTitle("Conversion Tracking", for: .normal) + button.titleLabel?.font = .systemFont(ofSize: 18, weight: .semibold) + button.backgroundColor = .systemBlue + button.tintColor = .white + button.layer.cornerRadius = 10 + button.addTarget(self, action: #selector(conversionTracking), for: .touchUpInside) + return button + }() + + private let shortLinkActivityIndicator = UIActivityIndicatorView(style: .medium) + private let secureLinkActivityIndicator = UIActivityIndicatorView(style: .medium) + + private let loadingShortLinkLabel: UILabel = { let label = UILabel() label.text = "Creating Short Link..." label.textAlignment = .center @@ -35,8 +58,26 @@ class ViewController: UIViewController { label.isHidden = true return label }() + + private let loadingSecuredShortLinkLabel: UILabel = { + let label = UILabel() + label.text = "Creating Secured Short Link..." + label.textAlignment = .center + label.textColor = .gray + label.font = .systemFont(ofSize: 14) + label.isHidden = true + return label + }() + + private let resultShortLinkLabel: UILabel = { + let label = UILabel() + label.textAlignment = .center + label.numberOfLines = 0 + label.textColor = .systemGreen + return label + }() - private let resultLabel: UILabel = { + private let resultSecureShortLinkLabel: UILabel = { let label = UILabel() label.textAlignment = .center label.numberOfLines = 0 @@ -44,7 +85,7 @@ class ViewController: UIViewController { return label }() - private let copyButton: UIButton = { + private let copyShortLinkButton: UIButton = { let button = UIButton(type: .system) button.setTitle("Copy Short Link", for: .normal) button.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium) @@ -52,11 +93,33 @@ class ViewController: UIViewController { button.tintColor = .white button.layer.cornerRadius = 8 button.isHidden = true - button.addTarget(self, action: #selector(copyToClipboard), for: .touchUpInside) + button.tag = 1 + button.addTarget(self, action: #selector(copyToClipboard(_:)), for: .touchUpInside) return button }() - private let errorLabel: UILabel = { + private let copySecureShortLinkButton: UIButton = { + let button = UIButton(type: .system) + button.setTitle("Copy Secure Short Link", for: .normal) + button.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium) + button.backgroundColor = .systemGray + button.tintColor = .white + button.layer.cornerRadius = 8 + button.isHidden = true + button.tag = 2 + button.addTarget(self, action: #selector(copyToClipboard(_:)), for: .touchUpInside) + return button + }() + + private let errorShortLinkLabel: UILabel = { + let label = UILabel() + label.textAlignment = .center + label.numberOfLines = 0 + label.textColor = .systemRed + return label + }() + + private let errorSecureShortLinkLabel: UILabel = { let label = UILabel() label.textAlignment = .center label.numberOfLines = 0 @@ -71,21 +134,35 @@ class ViewController: UIViewController { } private func layoutUI() { - let loaderStack = UIStackView(arrangedSubviews: [activityIndicator, loadingLabel]) - loaderStack.axis = .vertical - loaderStack.alignment = .center - loaderStack.spacing = 8 + let shortLinkLoaderStack = UIStackView(arrangedSubviews: [shortLinkActivityIndicator, loadingShortLinkLabel]) + shortLinkLoaderStack.axis = .vertical + shortLinkLoaderStack.alignment = .center + shortLinkLoaderStack.spacing = 8 + + let secureLinkLoaderStack = UIStackView(arrangedSubviews: [secureLinkActivityIndicator, loadingSecuredShortLinkLabel]) + secureLinkLoaderStack.axis = .vertical + secureLinkLoaderStack.alignment = .center + secureLinkLoaderStack.spacing = 8 let stackView = UIStackView(arrangedSubviews: [ titleLabel, - createButton, - loaderStack, - resultLabel, - copyButton, - errorLabel + + createShortLinkButton, + shortLinkLoaderStack, + resultShortLinkLabel, + copyShortLinkButton, + errorShortLinkLabel, + + createSecureShortLinkButton, + secureLinkLoaderStack, + resultSecureShortLinkLabel, + copySecureShortLinkButton, + errorSecureShortLinkLabel, + + conversionTrackingButton ]) stackView.axis = .vertical - stackView.spacing = 20 + stackView.spacing = 10 stackView.alignment = .center stackView.translatesAutoresizingMaskIntoConstraints = false @@ -95,61 +172,109 @@ class ViewController: UIViewController { stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20), stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20), stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor), - createButton.widthAnchor.constraint(equalTo: stackView.widthAnchor), - copyButton.widthAnchor.constraint(equalTo: stackView.widthAnchor) + + createShortLinkButton.widthAnchor.constraint(equalTo: stackView.widthAnchor), + copyShortLinkButton.widthAnchor.constraint(equalTo: stackView.widthAnchor), + createSecureShortLinkButton.widthAnchor.constraint(equalTo: stackView.widthAnchor), + copySecureShortLinkButton.widthAnchor.constraint(equalTo: stackView.widthAnchor), + conversionTrackingButton.widthAnchor.constraint(equalTo: stackView.widthAnchor), ]) } - @objc private func copyToClipboard() { - guard let text = resultLabel.text?.replacingOccurrences(of: "Short URL: ", with: "") else { return } - UIPasteboard.general.string = text - let alert = UIAlertController(title: "Copied", message: "Short URL copied to clipboard.", preferredStyle: .alert) + @objc private func copyToClipboard(_ sender: UIButton) { + let text: String? + let message: String + + switch sender.tag { + case 1: + text = resultShortLinkLabel.text?.replacingOccurrences(of: "Short URL: ", with: "") + message = "Short URL copied to clipboard." + case 2: + text = resultSecureShortLinkLabel.text?.replacingOccurrences(of: "Secured Short URL: ", with: "") + message = "Secured Short URL copied to clipboard." + default: + return + } + + guard let copiedText = text, !copiedText.isEmpty else { return } + + UIPasteboard.general.string = copiedText + + let alert = UIAlertController(title: "Copied", message: message, preferredStyle: .alert) alert.addAction(UIAlertAction(title: "OK", style: .default)) present(alert, animated: true) } @objc private func createShortLink() { - resultLabel.text = nil - errorLabel.text = nil - copyButton.isHidden = true - activityIndicator.startAnimating() - loadingLabel.isHidden = false - createButton.isEnabled = false + resultShortLinkLabel.text = nil + errorShortLinkLabel.text = nil + copyShortLinkButton.isHidden = true + shortLinkActivityIndicator.startAnimating() + loadingShortLinkLabel.isHidden = false + createShortLinkButton.isEnabled = false let parameters: ShortIOParameters do { parameters = try ShortIOParameters( - domain: "your_domain", - originalURL: "your_original_url" + originalURL: "{https://{your_domain}" ) } catch { - activityIndicator.stopAnimating() - loadingLabel.isHidden = true - createButton.isEnabled = true - errorLabel.text = "Invalid input: \(error.localizedDescription)" + shortLinkActivityIndicator.stopAnimating() + createShortLinkButton.isEnabled = true + errorShortLinkLabel.text = "Invalid input: \(error.localizedDescription)" return } - let apiKey = "your_api_key" - Task { @MainActor in do { - let result = try await shortLinkSDK.createShortLink(parameters: parameters, apiKey: apiKey) + let result = try await shortLinkSDK.createShortLink(parameters: parameters) switch result { case .success(let response): - resultLabel.text = "Short URL: \(response.shortURL)" - copyButton.isHidden = false + resultShortLinkLabel.text = "Short URL: \(response.shortURL)" + copyShortLinkButton.isHidden = false case .failure(let errorResponse): - errorLabel.text = "Error: \(errorResponse.message)" + errorShortLinkLabel.text = "Error: \(errorResponse.message)" } } catch { - errorLabel.text = "Error: \(error.localizedDescription)" + errorShortLinkLabel.text = "Error: \(error.localizedDescription)" } - activityIndicator.stopAnimating() - loadingLabel.isHidden = true - createButton.isEnabled = true + shortLinkActivityIndicator.stopAnimating() + createShortLinkButton.isEnabled = true + loadingShortLinkLabel.isHidden = true + } + } + + @objc private func createSecureShortLink() { + resultSecureShortLinkLabel.text = nil + errorSecureShortLinkLabel.text = nil + copySecureShortLinkButton.isHidden = true + secureLinkActivityIndicator.startAnimating() + loadingSecuredShortLinkLabel.isHidden = false + createSecureShortLinkButton.isEnabled = false + + Task { @MainActor in + do { + let result = try await shortLinkSDK.createSecure(originalURL: "https://{your_domain}") + resultSecureShortLinkLabel.text = "Secured Short URL: \(result.securedShortUrl)" + copySecureShortLinkButton.isHidden = false + } catch { + errorSecureShortLinkLabel.text = "Error: \(error.localizedDescription)" + } + secureLinkActivityIndicator.stopAnimating() + createSecureShortLinkButton.isEnabled = true + loadingSecuredShortLinkLabel.isHidden = true } } -} + @objc private func conversionTracking() { + Task { + do { + let result = try await shortLinkSDK.trackConversion(clid: "your_clid", domain: "your_domain", conversionId: "your_conversion_id") + print("result", result) + } catch { + print("Failed to track conversion: \(error)") + } + } + } +} diff --git a/SwiftUIProject/ShortIOApp/AppDelegate.swift b/SwiftUIProject/ShortIOApp/AppDelegate.swift new file mode 100644 index 0000000..4dc26cf --- /dev/null +++ b/SwiftUIProject/ShortIOApp/AppDelegate.swift @@ -0,0 +1,19 @@ +import Foundation +import SwiftUI +import ShortIOSDK + + +class AppDelegate: NSObject, UIApplicationDelegate { + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool { + + let sdk = ShortIOSDK.shared + + sdk.initialize(apiKey: "your-api-key-here", domain: "your-domain-here") + + // Override point for customization after application launch. + return true + } + +} + diff --git a/SwiftUIProject/ShortIOApp/ContentView.swift b/SwiftUIProject/ShortIOApp/ContentView.swift index c5fade0..90968ea 100644 --- a/SwiftUIProject/ShortIOApp/ContentView.swift +++ b/SwiftUIProject/ShortIOApp/ContentView.swift @@ -6,18 +6,19 @@ struct ContentView: View { @State private var shortURL: String? @State private var errorMessage: String? @State private var isLoading: Bool = false - private let shortLinkSDK = ShortIOSDK() - + @State private var secureShortURL: String? + private let shortLinkSDK = ShortIOSDK.shared // Ensure ShortLinkSDK is accessible + var body: some View { VStack(spacing: 20) { Image(systemName: "globe") .imageScale(.large) .foregroundStyle(.tint) - + Text("Short Link Generator") .font(.title) .fontWeight(.bold) - + if isLoading { ProgressView("Creating short link...") } else { @@ -31,8 +32,30 @@ struct ContentView: View { .cornerRadius(10) } .padding(.horizontal) + + Button(action: createEncryptedLink) { + Text("Create Encrypted Short Link") + .font(.headline) + .padding() + .frame(maxWidth: .infinity) + .background(Color.blue) + .foregroundColor(.white) + .cornerRadius(10) + } + .padding(.horizontal) + + Button(action: conversionTracking) { + Text("Create conversion Tracking") + .font(.headline) + .padding() + .frame(maxWidth: .infinity) + .background(Color.blue) + .foregroundColor(.white) + .cornerRadius(10) + } + .padding(.horizontal) } - + if let shortURL = shortURL { Text("Short URL: \(shortURL)") .font(.subheadline) @@ -47,7 +70,22 @@ struct ContentView: View { } } } - + + if let secureShortURL = secureShortURL { + Text("Secure Short URL: \(secureShortURL)") + .font(.subheadline) + .foregroundColor(.green) + .padding() + .contextMenu { + Button(action: { + UIPasteboard.general.string = secureShortURL + }) { + Text("Copy to Clipboard") + Image(systemName: "doc.on.doc") + } + } + } + if let errorMessage = errorMessage { Text("Error: \(errorMessage)") .font(.subheadline) @@ -57,23 +95,20 @@ struct ContentView: View { } .padding() } - + private func createShortLink() { isLoading = true shortURL = nil errorMessage = nil - + let parameters = ShortIOParameters( - domain: "your_domain", - originalURL: "https://{your_domain}" + originalURL:"your-original-url-here" ) - let apiKey = "your_api_key" - - Task { @MainActor in + + Task { do { let result = try await shortLinkSDK.createShortLink( - parameters: parameters, - apiKey: apiKey + parameters: parameters ) switch result { case .success(let response): @@ -90,6 +125,29 @@ struct ContentView: View { isLoading = false } } + + private func createEncryptedLink() { + Task { + do { + let result = try shortLinkSDK.createSecure(originalURL: "your_original_url") + secureShortURL = result.securedOriginalURL + print("result", result.securedOriginalURL, result.securedShortUrl) + } catch { + print("Failed to create secure URL: \(error)") + } + } + } + + private func conversionTracking() { + Task { + do { + let result = try await shortLinkSDK.trackConversion(clid: "your_clid", domain: "your_domain", conversionId: "your_conversion_id") + print("result", result) + } catch { + print("Failed to track conversion: \(error)") + } + } + } } #Preview { diff --git a/SwiftUIProject/ShortIOApp/ShortIOApp.swift b/SwiftUIProject/ShortIOApp/ShortIOApp.swift index 950f8dd..d287bab 100644 --- a/SwiftUIProject/ShortIOApp/ShortIOApp.swift +++ b/SwiftUIProject/ShortIOApp/ShortIOApp.swift @@ -3,15 +3,24 @@ import ShortIOSDK @main struct ShortIOApp: App { - - var sdk = ShortIOSDK() + + @UIApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate + + var sdk = ShortIOSDK.shared var body: some Scene { WindowGroup { ContentView() .onOpenURL { url in sdk.handleOpen(url) { result in - print("Host: \(result?.host), Path: \(result?.path)") + switch result { + case .success(let result): + // Handle successful URL processing + print("result", result, "Host: \(result.host), Path: \(result.path)", "QueryParams: \(result.queryItems)") + case .failure(let error): + // Handle error with proper error type + print("Error: \(error.localizedDescription)") + } } } }