diff --git a/README.md b/README.md index 4d5748d..d4643c9 100644 --- a/README.md +++ b/README.md @@ -69,35 +69,52 @@ struct PreLandingView: View { /* ... */ .onAppear { Task { - // 1.- Search for post-install link and proceed if available - guard let result = try? await traceback.postInstallSearchLink(), - let tracebackURL = result.url else { - return + do { + // 1.- Search for post-install link and proceed if available + let result = try await traceback.postInstallSearchLink() + if let tracebackURL = result.url { + proceed(onOpenURL: tracebackURL) + } + } catch { + // Handle error - network issues, configuration problems, etc. + logger.error("Failed to search for post-install link: \(error)") } - proceed(onOpenURL: tracebackURL) } } .onOpenURL { url in proceed(onOpenURL: url) } } - + // This method is to be called from onOpenURL or after post install link search - func proceed( - onOpenURL: URL - ) { - // 2.- Grab the correct url - // URL is either a post-install link (detected after app download on onAppear above), - // or an opened url (direct open in installed app) - guard let linkResult = try? traceback.extractLinkFromURL(url), - let linkURL = linkResult.url else { - return assertionFailure("Could not find a valid traceback/universal url in \(url)") + func proceed(onOpenURL url: URL) { + // 2.- Check if this is a Traceback URL + guard traceback.isTracebackURL(url) else { + // Not a Traceback URL, handle it elsewhere + handleDeepLink(linkURL) + return + } + + Task { + do { + // 3.- Check if dynamic campaign link exists (resolves the deep link from the URL) + let linkResult = try await traceback.campaignSearchLink(url) + + guard let linkURL = linkResult.url else { + // No deep link found in this URL, so we normally continue opening the app Landing screen + return + } + + // 4.- Handle the url, opening the right content indicated by linkURL + // Use linkURL to navigate to the appropriate content in your app + // You can also access linkResult.analytics for tracking purposes + handleDeepLink(linkURL) + sendAnalytics(linkResult.analytics) + } catch { + // Handle error - network issues, invalid URL, etc. + logger.error("Failed to resolve campaign link: \(error)") + } } - - // 3.- Handle the url, opening the right content indicated by linkURL - // Use linkURL to navigate to the appropriate content in your app - // You can also access linkResult.analytics for tracking purposes - YOUR CODE HERE } } ``` @@ -114,13 +131,17 @@ class YourAppDelegate: NSObject, UIApplicationDelegate { didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil ) -> Bool { Task { - // 1.- Trigger a search for installation links - // if a link is found successfully, it will be sent to proceed(openURL:) below - guard let result = try? await traceback.postInstallSearchLink(), - let tracebackURL = result.url else { - return + do { + // 1.- Trigger a search for installation links + // if a link is found successfully, it will be sent to proceed(openURL:) below + let result = try await traceback.postInstallSearchLink() + if let tracebackURL = result.url { + proceed(onOpenURL: tracebackURL) + } + } catch { + // Handle error - network issues, configuration problems, etc. + logger.error("Failed to search for post-install link: \(error)") } - proceed(onOpenURL: tracebackURL) } return true } @@ -135,23 +156,36 @@ class YourAppDelegate: NSObject, UIApplicationDelegate { proceed(onOpenURL: url) return true } - + // This method is to be called from application(open:options:) or after post install link search - func proceed( - onOpenURL: URL - ) { - // 2.- Grab the correct url - // URL is either a post-install link (detected after app launch above), - // or an opened url (direct open in installed app) - guard let linkResult = try? traceback.extractLinkFromURL(url), - let linkURL = linkResult.url else { - return assertionFailure("Could not find a valid traceback/universal url in \(url)") + func proceed(onOpenURL url: URL) { + // 2.- Check if this is a Traceback URL + guard traceback.isTracebackURL(url) else { + // Not a Traceback URL, handle it elsewhere + handleDeepLink(linkURL) + return + } + + Task { + do { + // 3.- Check if dynamic campaign link exists (resolves the deep link from the URL) + let linkResult = try await traceback.campaignSearchLink(url) + + guard let linkURL = linkResult.url else { + // No deep link found in this URL, so we normally continue opening the app Landing screen + return + } + + // 4.- Handle the url, opening the right content indicated by linkURL + // Use linkURL to navigate to the appropriate content in your app + // You can also access linkResult.analytics for tracking purposes + handleDeepLink(linkURL) + sendAnalytics(linkResult.analytics) + } catch { + // Handle error - network issues, invalid URL, etc. + logger.error("Failed to resolve campaign link: \(error)") + } } - - // 3.- Handle the url, opening the right content indicated by linkURL - // Use linkURL to navigate to the appropriate content in your app - // You can also access linkResult.analytics for tracking purposes - YOUR CODE HERE } } ``` @@ -191,12 +225,26 @@ The diagnostics will categorize issues as: ## API Reference +### TracebackSDK Methods + +#### `postInstallSearchLink() async throws -> TracebackSDK.Result` +Searches for the deep link that triggered the app installation. Call this once during app launch. + +#### `campaignSearchLink(_ url: URL) async throws -> TracebackSDK.Result` +Resolves a Traceback URL opened via Universal Link or custom URL scheme into a deep link. + +#### `isTracebackURL(_ url: URL) -> Bool` +Validates if the given URL matches any of the configured Traceback domains. + +#### `performDiagnostics()` +Runs comprehensive validation of your Traceback configuration and outputs diagnostic information. + ### TracebackSDK.Result -The result object returned by `postInstallSearchLink()` and `extractLinkFromURL()` contains: +The result object returned by `postInstallSearchLink()` and `campaignSearchLink()` contains: - `url: URL?` - The extracted deep link URL to navigate to -- `matchType: MatchType` - How the link was detected (`.unique`, `.heuristics`, `.ambiguous`, `.intent`, `.none`) +- `matchType: MatchType` - How the link was detected (`.unique`, `.heuristics`, `.ambiguous`, `.intent`, `.none`, `.unknown`) - `analytics: [TracebackAnalyticsEvent]` - Analytics events you can send to your preferred platform ### TracebackConfiguration @@ -220,7 +268,9 @@ public struct TracebackConfiguration { ## Error Handling -The SDK uses Swift's error handling mechanisms: +The SDK uses Swift's error handling mechanisms. Both `postInstallSearchLink()` and `campaignSearchLink()` can throw errors: + +### Post-Install Link Search ```swift do { @@ -228,6 +278,8 @@ do { if let url = result.url { // Handle successful link detection handleDeepLink(url) + // Send analytics events + sendAnalytics(result.analytics) } else { // No link found - normal app startup handleNormalStartup() @@ -239,6 +291,26 @@ do { } ``` +### Campaign Link Resolution + +```swift +do { + let result = try await traceback.campaignSearchLink(url) + if let deepLink = result.url { + // Handle successful link resolution + handleDeepLink(deepLink) + // Send analytics events + sendAnalytics(result.analytics) + } else { + // URL is valid Traceback URL but no deep link found + handleNormalStartup() + } +} catch { + // Handle network or configuration errors + logger.error("Failed to resolve campaign link: \(error)") +} +``` + ## Troubleshooting ### Common Issues diff --git a/agents.md b/agents.md new file mode 100644 index 0000000..161e3d3 --- /dev/null +++ b/agents.md @@ -0,0 +1,6 @@ +Traceback ios is a companion sdk to communicate with traceback firebase extension +This sdk is installed via SPM in other projects + +README.md shuold give enough onboarding information + +don't make authoring header files or commit messages with agent information diff --git a/e2e/flows/fresh_install.yaml b/e2e/flows/fresh_install.yaml new file mode 100644 index 0000000..c46a34c --- /dev/null +++ b/e2e/flows/fresh_install.yaml @@ -0,0 +1,9 @@ +# fresh_install.yaml + +appId: ${BUNDLE_ID} +--- +- launchApp +- tapOn: 'Permitir pegar' +- assertVisible: + text: '.*${DESTINATION_URL}' + index: 0 diff --git a/e2e/flows/open_link_in_safari.yaml b/e2e/flows/open_link_in_safari.yaml new file mode 100644 index 0000000..e30468b --- /dev/null +++ b/e2e/flows/open_link_in_safari.yaml @@ -0,0 +1,10 @@ +# flow.yaml + +appId: com.apple.mobilesafari +--- +- launchApp +- extendedWaitUntil: + visible: "OPEN" + timeout: 10000 +- tapOn: "OPEN" +- tapOn: "OK" diff --git a/samples/README.md b/samples/README.md new file mode 100644 index 0000000..70e5e5a --- /dev/null +++ b/samples/README.md @@ -0,0 +1,80 @@ +# Traceback iOS SDK - Sample Applications + +This directory contains sample applications demonstrating how to integrate and use the Traceback iOS SDK in different scenarios. + +## Available Samples + +### [swiftui-basic](./swiftui-basic/) +A basic SwiftUI application demonstrating: +- Standard Traceback SDK configuration +- Post-install link detection +- Universal Link handling with campaign resolution +- Deep link navigation +- Analytics event tracking +- Diagnostics setup + +**Best for:** Getting started with Traceback in a SwiftUI project + +### Coming Soon + +- **uikit-basic** - Basic UIKit implementation +- **advanced-analytics** - Advanced analytics integration +- **custom-domains** - Multiple domain configuration +- **clipboard-disabled** - Privacy-focused setup without clipboard + +## Prerequisites + +Before running any sample: + +1. **Install the Traceback Firebase Extension** in your Firebase project + - Follow instructions at: https://github.com/InQBarna/firebase-traceback-extension + +2. **Configure Firebase** for your sample app + - Create an iOS app in your Firebase Console + - Download `GoogleService-Info.plist` + +3. **Set up Associated Domains** + - Configure your Apple Developer account + - Enable Associated Domains capability + - Add the Traceback domain from your Firebase extension + +## Running a Sample + +Each sample includes its own README with specific setup instructions. Generally: + +1. Navigate to the sample directory +2. Follow the README to configure Firebase settings +3. Open the `.xcodeproj` or `.xcworkspace` in Xcode +4. Update the bundle identifier and signing team +5. Run the app on a physical device (Universal Links don't work in Simulator) + +## Testing Deep Links + +### Create a Test Link + +Use the Traceback Firebase extension to create a test deep link: + +```bash +# Example: Create a link to open /products/123 in your app +https://your-project-traceback.firebaseapp.com/campaign-name?link=myapp://products/123 +``` + +### Test Post-Install Flow + +1. Copy the Traceback link to clipboard +2. Delete the app from your device +3. Install and launch the app +4. The app should detect and open the deep link + +### Test Campaign Links + +1. Send yourself the Traceback link via Messages/Email +2. Open the link with the app already installed +3. The app should resolve and open the deep link + +## Need Help? + +- Check the main [README](../README.md) for SDK documentation +- Review the [Troubleshooting](../README.md#troubleshooting) section +- Run `traceback.performDiagnostics()` to validate your setup +- Report issues at: https://github.com/InQBarna/traceback-iOS/issues diff --git a/samples/e2e-build-and-run.sh b/samples/e2e-build-and-run.sh new file mode 100755 index 0000000..271a92a --- /dev/null +++ b/samples/e2e-build-and-run.sh @@ -0,0 +1,146 @@ +#!/bin/bash + +# Script to build and run the Traceback SwiftUI Sample on iOS Simulator +# Usage: ./build-and-run.sh [DEVICE_NAME] [PROJECT_PATH] +# +# Examples: +# ./build-and-run.sh "iPhone 15 Pro" +# ./build-and-run.sh "iPhone 15 Pro" /path/to/TracebackSwiftUIExample.xcodeproj +# ./build-and-run.sh # Uses defaults + +set -e # Exit on error + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Default values +DEFAULT_DEVICE="iPhone 15 Pro" +DEFAULT_PROJECT_PATH="swiftui-basic/TracebackSwiftUIExample/TracebackSwiftUIExample.xcodeproj" +SCHEME_NAME="TracebackSwiftUIExample" + +# Parse arguments +DEVICE_NAME="${1:-$DEFAULT_DEVICE}" +PROJECT_PATH="${2:-$DEFAULT_PROJECT_PATH}" + +# Functions +print_info() { + echo -e "${GREEN}[INFO]${NC} $1" +} + +print_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +print_warning() { + echo -e "${YELLOW}[WARNING]${NC} $1" +} + +# Validate project exists +if [ ! -d "$PROJECT_PATH" ]; then + print_error "Project not found at: $PROJECT_PATH" + exit 1 +fi + +print_info "Building Traceback SwiftUI Sample" +print_info "Device: $DEVICE_NAME" +print_info "Project: $PROJECT_PATH" +echo "" + +# Get the device UDID +print_info "Finding simulator..." +DEVICE_UDID=$(xcrun simctl list devices available | grep "$DEVICE_NAME" | head -n 1 | grep -o -E '\([A-F0-9-]+\)' | tr -d '()') + +if [ -z "$DEVICE_UDID" ]; then + print_error "Simulator '$DEVICE_NAME' not found" + echo "" + print_info "Available simulators:" + xcrun simctl list devices available | grep iPhone + exit 1 +fi + +print_info "Found simulator: $DEVICE_NAME ($DEVICE_UDID)" + +# Boot simulator if not already running +print_info "Booting simulator..." +xcrun simctl boot "$DEVICE_UDID" 2>/dev/null || true +open -a Simulator + +# Wait for simulator to boot +print_info "Waiting for simulator to boot..." +while [ "$(xcrun simctl list devices | grep "$DEVICE_UDID" | grep -c "Booted")" -eq 0 ]; do + sleep 1 +done +print_info "Simulator booted" + +# Clean build folder (optional, comment out for faster rebuilds) +print_info "Cleaning build folder..." +xcodebuild clean \ + -project "$PROJECT_PATH" \ + -scheme "$SCHEME_NAME" \ + -configuration Debug \ + > /dev/null 2>&1 + +# Build the project +print_info "Building project..." +BUILD_DIR=$(mktemp -d) +BUILD_LOG="$BUILD_DIR/build.log" + +xcodebuild build \ + -project "$PROJECT_PATH" \ + -scheme "$SCHEME_NAME" \ + -configuration Debug \ + -sdk iphonesimulator \ + -destination "platform=iOS Simulator,id=$DEVICE_UDID" \ + -derivedDataPath "$BUILD_DIR" \ + CODE_SIGNING_ALLOWED=NO \ + CODE_SIGN_IDENTITY="" \ + CODE_SIGN_ENTITLEMENTS="" \ + > "$BUILD_LOG" 2>&1 + +BUILD_STATUS=$? + +if [ $BUILD_STATUS -ne 0 ]; then + print_error "Build failed with status $BUILD_STATUS" + echo "" + print_info "Build errors:" + grep -E "error:" "$BUILD_LOG" | head -20 + echo "" + print_info "Full build log: $BUILD_LOG" + exit 1 +fi + +print_info "Build succeeded" + +# Find the .app bundle +APP_PATH=$(find "$BUILD_DIR" -name "*.app" -type d | head -n 1) + +if [ -z "$APP_PATH" ]; then + print_error "Failed to find .app bundle" + print_info "Build output in: $BUILD_DIR" + exit 1 +fi + +print_info "Build succeeded: $APP_PATH" + +# Install the app +print_info "Installing app on simulator..." +xcrun simctl install "$DEVICE_UDID" "$APP_PATH" + +# Get bundle identifier +BUNDLE_ID=$(defaults read "$APP_PATH/Info.plist" CFBundleIdentifier) + +# Cleanup +rm -rf "$BUILD_DIR" + +echo "" +print_info "✅ Successfully installed $SCHEME_NAME on $DEVICE_NAME" +print_info "Bundle ID: $BUNDLE_ID" +echo "" +print_info "To launch the app, tap its icon in the simulator or run:" +echo " xcrun simctl launch $DEVICE_UDID $BUNDLE_ID" +echo "" +print_warning "Note: Universal Links don't work in Simulator!" +print_warning "For full testing, deploy to a physical device." diff --git a/samples/e2e-uninstall.sh b/samples/e2e-uninstall.sh new file mode 100755 index 0000000..22b61bb --- /dev/null +++ b/samples/e2e-uninstall.sh @@ -0,0 +1,77 @@ +#!/bin/bash + +# Script to uninstall the Traceback SwiftUI Sample from iOS Simulator +# Usage: ./uninstall.sh [DEVICE_NAME] [BUNDLE_ID] +# +# Examples: +# ./uninstall.sh "iPhone 15 Pro" +# ./uninstall.sh "iPhone 15 Pro" com.custom.bundle.id +# ./uninstall.sh # Uses defaults + +set -e # Exit on error + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Default values +DEFAULT_DEVICE="iPhone 15 Pro" +DEFAULT_BUNDLE_ID="com.inqbarna.traceback.samples" + +# Parse arguments +DEVICE_NAME="${1:-$DEFAULT_DEVICE}" +BUNDLE_ID="${2:-$DEFAULT_BUNDLE_ID}" + +# Functions +print_info() { + echo -e "${GREEN}[INFO]${NC} $1" +} + +print_error() { + echo -e "${RED}[ERROR]${NC} $1" +} + +print_warning() { + echo -e "${YELLOW}[WARNING]${NC} $1" +} + +print_info "Uninstalling Traceback SwiftUI Sample" +print_info "Device: $DEVICE_NAME" +print_info "Bundle ID: $BUNDLE_ID" +echo "" + +# Get the device UDID +print_info "Finding simulator..." +DEVICE_UDID=$(xcrun simctl list devices available | grep "$DEVICE_NAME" | head -n 1 | grep -o -E '\([A-F0-9-]+\)' | tr -d '()') + +if [ -z "$DEVICE_UDID" ]; then + print_error "Simulator '$DEVICE_NAME' not found" + echo "" + print_info "Available simulators:" + xcrun simctl list devices available | grep iPhone + exit 1 +fi + +print_info "Found simulator: $DEVICE_NAME ($DEVICE_UDID)" + +# Check if app is installed +print_info "Checking if app is installed..." +APP_INSTALLED=$(xcrun simctl listapps "$DEVICE_UDID" | grep -c "$BUNDLE_ID" || true) + +if [ "$APP_INSTALLED" -eq 0 ]; then + print_warning "App with bundle ID '$BUNDLE_ID' is not installed on this simulator" + echo "" + print_info "Installed apps:" + xcrun simctl listapps "$DEVICE_UDID" | grep -E "Bundle|CFBundleIdentifier" | head -20 + exit 0 +fi + +# Uninstall the app +print_info "Uninstalling app..." +xcrun simctl uninstall "$DEVICE_UDID" "$BUNDLE_ID" + +echo "" +print_info "✅ Successfully uninstalled app from $DEVICE_NAME" +print_info "Bundle ID: $BUNDLE_ID" diff --git a/samples/swiftui-basic/QUICKSTART.md b/samples/swiftui-basic/QUICKSTART.md new file mode 100644 index 0000000..bc4dc1e --- /dev/null +++ b/samples/swiftui-basic/QUICKSTART.md @@ -0,0 +1,112 @@ +# Quick Start Guide + +Get the SwiftUI Basic sample running in 5 minutes. + +## Step 1: Open the Project + +```bash +open TracebackSwiftUIExample/TracebackSwiftUIExample.xcodeproj +``` + +## Step 2: Update Configuration + +In Xcode, you need to update 3 places with your Firebase Traceback domain: + +### 2.1 Update Bundle Identifier & Associated Domains + +1. Select `TracebackSwiftUIExample` project in Navigator +2. Select the `TracebackSwiftUIExample` target +3. Go to "Signing & Capabilities" tab +4. **Update your Team** for code signing +5. **Update Bundle Identifier** (e.g., `com.yourcompany.traceback.demo`) +6. In "Associated Domains", replace `your-project-traceback.firebaseapp.com` with your actual domain + +### 2.2 Update Entitlements File + +Open `TracebackSwiftUIExample/TracebackSwiftUIExample.entitlements` and update: + +```xml +applinks:YOUR-ACTUAL-DOMAIN.firebaseapp.com +``` + +### 2.3 Update SDK Configuration + +Open `TracebackSwiftUIExampleApp.swift` and update line ~29: + +```swift +mainAssociatedHost: URL(string: "https://YOUR-ACTUAL-DOMAIN.firebaseapp.com")!, +``` + +### 2.4 Optional: Update URL Scheme + +If you want a custom deep link scheme (instead of `myapp://`): + +1. Open `Info.plist` +2. Find `CFBundleURLSchemes` +3. Change `myapp` to your desired scheme + +## Step 3: Build and Run + +1. Connect a **physical iOS device** (Universal Links don't work in Simulator) +2. Select your device in Xcode +3. Build and Run (Cmd+R) +4. Check the console for diagnostics output + +## Step 4: Test Deep Links + +### Test Post-Install Detection + +1. Create a test link in your browser: + ``` + https://YOUR-DOMAIN.firebaseapp.com/welcome?link=myapp://home + ``` + +2. **Copy the link** to clipboard (Cmd+C) + +3. **Delete the app** from your device + +4. **Reinstall and launch** the app + +5. The app should detect the clipboard link and navigate to Home + +### Test Campaign Links + +1. Send yourself this link via Messages: + ``` + https://YOUR-DOMAIN.firebaseapp.com/promo?link=myapp://products/123 + ``` + +2. Tap the link with the app already installed + +3. The app should open and navigate to Product 123 + +## Troubleshooting + +### "Could not resolve package dependencies" + +The project references the Traceback SDK from GitHub. If you're testing locally: + +1. In Xcode, go to File → Swift Packages → Resolve Package Versions +2. Or, update the package reference to point to your local SDK: + - File → Swift Packages → Remove Package "Traceback" + - File → Add Packages → Add Local → Select your local traceback-iOS folder + +### "Universal Links not working" + +- Ensure you're testing on a **physical device** +- Check the Xcode console for diagnostics warnings +- Verify your Associated Domains are correct +- Try deleting and reinstalling the app + +### "No post-install link detected" + +- Make sure `useClipboard: true` in configuration +- Verify the link was copied before installing +- Check console logs for detailed information + +## Next Steps + +- Review the source code to understand the implementation +- Customize the deep link routes in `DeepLinkRoute.from()` +- Add your own analytics integration +- Explore other samples for advanced use cases diff --git a/samples/swiftui-basic/README.md b/samples/swiftui-basic/README.md new file mode 100644 index 0000000..2b7a763 --- /dev/null +++ b/samples/swiftui-basic/README.md @@ -0,0 +1,295 @@ +# SwiftUI Basic Sample + +A basic SwiftUI application demonstrating core Traceback SDK integration with comprehensive debugging. + +## Features + +- ✅ Traceback SDK configuration +- ✅ Post-install link detection +- ✅ Universal Link handling +- ✅ Campaign link resolution +- ✅ Analytics event tracking +- ✅ Diagnostics validation +- ✅ **SDK Debug UI** - View raw SDK method results in real-time + +## Prerequisites + +1. **Firebase Project** with Traceback extension installed + - Follow: https://github.com/InQBarna/firebase-traceback-extension + +2. **Apple Developer Account** with Associated Domains enabled + +3. **Physical iOS Device** (Universal Links don't work in Simulator) + +## Quick Start with Script + +The easiest way to build and install the sample is using the provided build script: + +```bash +cd samples +./build-and-run.sh "iPhone 15 Pro" +``` + +This will: +1. ✅ Boot the specified simulator +2. ✅ Build the project +3. ✅ Install the app + +Then you can launch it manually from the simulator home screen. + +### Script Usage + +**Build and Install:** +```bash +cd samples + +# Use default device (iPhone 15 Pro) and default project (swiftui-basic) +./build-and-run.sh + +# Specify a device +./build-and-run.sh "iPhone 14" + +# Specify device and custom project path +./build-and-run.sh "iPhone 15 Pro" swiftui-basic/TracebackSwiftUIExample/TracebackSwiftUIExample.xcodeproj +``` + +**Uninstall:** +```bash +cd samples + +# Use default device and bundle ID +./uninstall.sh + +# Specify a device +./uninstall.sh "iPhone 14" + +# Specify device and custom bundle ID +./uninstall.sh "iPhone 15 Pro" com.custom.bundle.id +``` + +**List available simulators:** +```bash +xcrun simctl list devices available | grep iPhone +``` + +**Note:** Universal Links don't work in the Simulator. For full testing, you need a physical device. + +--- + +## Manual Setup Instructions + +### 1. Firebase Configuration + +1. Create an iOS app in your Firebase Console +2. Download `GoogleService-Info.plist` (optional - only if you use Firebase in your app) +3. Note your Traceback domain from the extension setup (e.g., `your-project-traceback.firebaseapp.com`) + +### 2. Update App Configuration + +Open `TracebackSwiftUIExample.xcodeproj` in Xcode and: + +1. **Update Bundle Identifier** + - Select the project in Navigator + - Go to "Signing & Capabilities" tab + - Change bundle identifier to match your Firebase app + +2. **Configure Associated Domains** + - In "Signing & Capabilities", ensure "Associated Domains" is enabled + - Add your Traceback domain: + ``` + applinks:your-project-traceback.firebaseapp.com + ``` + +3. **Configure URL Schemes** + - Go to "Info" tab + - Expand "URL Types" + - Verify the URL scheme matches your bundle identifier + - Or create a custom scheme for deep linking (e.g., `myapp`) + +4. **Update Traceback Configuration** + - Open `TracebackSwiftUIExampleApp.swift` + - Update the `mainAssociatedHost` with your Traceback domain: + ```swift + let config = TracebackConfiguration( + mainAssociatedHost: URL(string: "https://your-project-traceback.firebaseapp.com")!, + useClipboard: true, + logLevel: .debug + ) + ``` + +### 3. Run Diagnostics + +1. Build and run on a physical device +2. Check Xcode console for diagnostics output +3. Resolve any errors or warnings reported + +## Testing + +### Test Post-Install Detection + +1. Create a test link in your Traceback extension: + ``` + https://your-project-traceback.firebaseapp.com/welcome?link=myapp://products/123 + ``` + +2. Copy the link to clipboard + +3. Delete the app from your device + +4. Install and launch the app + +5. The app should: + - Detect the clipboard link + - Show "Post-install link detected!" + - Display the deep link: `myapp://products/123` + +### Test Campaign Links (Already Installed) + +1. Create a campaign link: + ``` + https://your-project-traceback.firebaseapp.com/promo?link=myapp://settings + ``` + +2. Send the link to yourself via Messages or Email + +3. Tap the link with the app already installed + +4. The app should: + - Resolve the campaign link + - Show "Campaign link resolved!" + - Display the deep link: `myapp://settings` + +### Test Universal Links + +1. Create a Universal Link and host it on your website or use the Traceback domain + +2. Tap the link from Safari, Messages, or Email + +3. The app should open and handle the link + +## Project Structure + +``` +TracebackSwiftUIExample/ +├── TracebackSwiftUIExampleApp.swift # App entry point, SDK setup, and AppState +├── AppState+DeepLink.swift # Deep link handling methods (extension) +├── ContentView.swift # Main UI +└── Info.plist # URL schemes configuration +``` + +## Debug UI + +The app displays real-time SDK debugging information: + +### SDK Results Panel +- **`isTracebackURL`** - Shows `true`/`false` result for opened URLs +- **`postInstallSearchLink()`** - Displays the URL returned from post-install detection (or `nil`) +- **`campaignSearchLink()`** - Displays the URL returned from campaign resolution (or `nil`) + +### Debug Status +- Shows current SDK operation with emoji indicators: + - ✅ Success + - ❌ Error + - ℹ️ Info + - ⏳ Loading + +### Analytics Events +- All SDK analytics events are logged in real-time +- Shows events from both post-install and campaign methods + +This makes it easy to: +- Verify which SDK method returned a link +- Debug URL validation logic +- Understand the SDK's behavior step-by-step + +## Key Implementation Details + +### SDK Initialization + +The SDK is initialized in `TracebackSwiftUIExampleApp.swift`: + +```swift +lazy var traceback: TracebackSDK = { + let config = TracebackConfiguration( + mainAssociatedHost: URL(string: "https://your-project-traceback.firebaseapp.com")!, + useClipboard: true, + logLevel: .debug + ) + return TracebackSDK.live(config: config) +}() +``` + +### Post-Install Detection + +Called once on app launch in `ContentView.onAppear`: + +```swift +.onAppear { + Task { + do { + let result = try await traceback.postInstallSearchLink() + if let url = result.url { + handleDeepLink(url) + sendAnalytics(result.analytics) + } + } catch { + logger.error("Post-install search failed: \(error)") + } + } +} +``` + +### Universal Link Handling + +Handled via `onOpenURL` modifier: + +```swift +.onOpenURL { url in + guard traceback.isTracebackURL(url) else { return } + + Task { + do { + let result = try await traceback.campaignSearchLink(url) + if let deepLink = result.url { + handleDeepLink(deepLink) + sendAnalytics(result.analytics) + } + } catch { + logger.error("Campaign link resolution failed: \(error)") + } + } +} +``` + +## Troubleshooting + +### Universal Links not working + +- ✅ Ensure you're testing on a physical device (not Simulator) +- ✅ Verify Associated Domains are configured correctly +- ✅ Run `traceback.performDiagnostics()` to check setup +- ✅ Check that your app is signed with the correct team + +### Post-install detection not working + +- ✅ Verify `useClipboard: true` in configuration +- ✅ Ensure the link is copied to clipboard before install +- ✅ Check console logs for diagnostic information + +### Build errors + +- ✅ Ensure you're using Xcode 15+ and iOS 15+ deployment target +- ✅ Verify Traceback SDK package dependency is resolved +- ✅ Clean build folder (Cmd+Shift+K) and rebuild + +## Next Steps + +- Review the [main SDK documentation](../../README.md) +- Explore other samples (UIKit, advanced analytics, etc.) +- Customize the deep link navigation for your app's needs +- Integrate with your analytics platform + +## Support + +- Report issues: https://github.com/InQBarna/traceback-iOS/issues +- Main documentation: https://github.com/InQBarna/traceback-iOS diff --git a/samples/swiftui-basic/TracebackSwiftUIExample/TracebackSwiftUIExample.xcodeproj/project.pbxproj b/samples/swiftui-basic/TracebackSwiftUIExample/TracebackSwiftUIExample.xcodeproj/project.pbxproj new file mode 100644 index 0000000..b4f9788 --- /dev/null +++ b/samples/swiftui-basic/TracebackSwiftUIExample/TracebackSwiftUIExample.xcodeproj/project.pbxproj @@ -0,0 +1,376 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + 1A0001 /* TracebackSwiftUIExampleApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A0002 /* TracebackSwiftUIExampleApp.swift */; }; + 1A0003 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A0004 /* ContentView.swift */; }; + 1A0005 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 1A0006 /* Assets.xcassets */; }; + 1A0007 /* Traceback in Frameworks */ = {isa = PBXBuildFile; productRef = 1A0008 /* Traceback */; }; + 1A0020 /* AppState+DeepLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A0021 /* AppState+DeepLink.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 1A0002 /* TracebackSwiftUIExampleApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TracebackSwiftUIExampleApp.swift; sourceTree = ""; }; + 1A0004 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + 1A0006 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 1A0009 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 1A000A /* TracebackSwiftUIExample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TracebackSwiftUIExample.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 1A000B /* TracebackSwiftUIExample.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = TracebackSwiftUIExample.entitlements; sourceTree = ""; }; + 1A0021 /* AppState+DeepLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AppState+DeepLink.swift"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 1A000C /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 1A0007 /* Traceback in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 1A000D = { + isa = PBXGroup; + children = ( + 1A000E /* TracebackSwiftUIExample */, + 1A000F /* Products */, + ); + sourceTree = ""; + }; + 1A000E /* TracebackSwiftUIExample */ = { + isa = PBXGroup; + children = ( + 1A000B /* TracebackSwiftUIExample.entitlements */, + 1A0002 /* TracebackSwiftUIExampleApp.swift */, + 1A0021 /* AppState+DeepLink.swift */, + 1A0004 /* ContentView.swift */, + 1A0006 /* Assets.xcassets */, + 1A0009 /* Info.plist */, + ); + path = TracebackSwiftUIExample; + sourceTree = ""; + }; + 1A000F /* Products */ = { + isa = PBXGroup; + children = ( + 1A000A /* TracebackSwiftUIExample.app */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 1A0010 /* TracebackSwiftUIExample */ = { + isa = PBXNativeTarget; + buildConfigurationList = 1A0011 /* Build configuration list for PBXNativeTarget "TracebackSwiftUIExample" */; + buildPhases = ( + 1A0012 /* Sources */, + 1A000C /* Frameworks */, + 1A0013 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = TracebackSwiftUIExample; + packageProductDependencies = ( + 1A0008 /* Traceback */, + ); + productName = TracebackSwiftUIExample; + productReference = 1A000A /* TracebackSwiftUIExample.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 1A0014 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1500; + LastUpgradeCheck = 1500; + TargetAttributes = { + 1A0010 = { + CreatedOnToolsVersion = 15.0; + }; + }; + }; + buildConfigurationList = 1A0015 /* Build configuration list for PBXProject "TracebackSwiftUIExample" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 1A000D; + packageReferences = ( + 1A0016 /* XCRemoteSwiftPackageReference "traceback-iOS" */, + ); + productRefGroup = 1A000F /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 1A0010 /* TracebackSwiftUIExample */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 1A0013 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 1A0005 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 1A0012 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 1A0003 /* ContentView.swift in Sources */, + 1A0020 /* AppState+DeepLink.swift in Sources */, + 1A0001 /* TracebackSwiftUIExampleApp.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 1A0017 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 1A0018 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 1A0019 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = TracebackSwiftUIExample/TracebackSwiftUIExample.entitlements; + CODE_SIGN_STYLE = Manual; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = TracebackSwiftUIExample/Info.plist; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.inqbarna.traceback.samples; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 1A001A /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = TracebackSwiftUIExample/TracebackSwiftUIExample.entitlements; + CODE_SIGN_STYLE = Manual; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = TracebackSwiftUIExample/Info.plist; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.inqbarna.traceback.samples; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 1A0011 /* Build configuration list for PBXNativeTarget "TracebackSwiftUIExample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 1A0019 /* Debug */, + 1A001A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 1A0015 /* Build configuration list for PBXProject "TracebackSwiftUIExample" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 1A0017 /* Debug */, + 1A0018 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 1A0016 /* XCRemoteSwiftPackageReference "traceback-iOS" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/InQBarna/traceback-iOS"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 0.5.0; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 1A0008 /* Traceback */ = { + isa = XCSwiftPackageProductDependency; + package = 1A0016 /* XCRemoteSwiftPackageReference "traceback-iOS" */; + productName = Traceback; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 1A0014 /* Project object */; +} diff --git a/samples/swiftui-basic/TracebackSwiftUIExample/TracebackSwiftUIExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/samples/swiftui-basic/TracebackSwiftUIExample/TracebackSwiftUIExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/samples/swiftui-basic/TracebackSwiftUIExample/TracebackSwiftUIExample.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/samples/swiftui-basic/TracebackSwiftUIExample/TracebackSwiftUIExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/samples/swiftui-basic/TracebackSwiftUIExample/TracebackSwiftUIExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..bf273b6 --- /dev/null +++ b/samples/swiftui-basic/TracebackSwiftUIExample/TracebackSwiftUIExample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,15 @@ +{ + "originHash" : "da14aa4c0ceb4370e18b620abeb611396c07bac1e2daf7ae45770f9925eb9b43", + "pins" : [ + { + "identity" : "traceback-ios", + "kind" : "remoteSourceControl", + "location" : "https://github.com/InQBarna/traceback-iOS", + "state" : { + "revision" : "948d0ef95b2373867b018120841d662fb75a81f4", + "version" : "0.5.0" + } + } + ], + "version" : 3 +} diff --git a/samples/swiftui-basic/TracebackSwiftUIExample/TracebackSwiftUIExample/AppState+DeepLink.swift b/samples/swiftui-basic/TracebackSwiftUIExample/TracebackSwiftUIExample/AppState+DeepLink.swift new file mode 100644 index 0000000..57633a7 --- /dev/null +++ b/samples/swiftui-basic/TracebackSwiftUIExample/TracebackSwiftUIExample/AppState+DeepLink.swift @@ -0,0 +1,66 @@ +// +// AppState+DeepLink.swift +// TracebackSwiftUIExample +// +// Deep link handling methods for AppState +// + +import Foundation + +extension AppState { + + /// Checks for post-install link on app launch + /// Should be called only once when the app first appears + func checkPostInstallLink() async { + debugMessage = "Checking for post-install link..." + + do { + let result = try await traceback.postInstallSearchLink() + + if let result, let url = result.url { + debugMessage = "✅ Post-install link detected!" + handlePostInstallLink(url) + sendAnalytics(result.analytics) + } else { + debugMessage = "No post-install link found" + } + } catch { + debugMessage = "❌ Post-install check failed: \(error.localizedDescription)" + print("[Error] Post-install search failed: \(error)") + } + } + + /// Handles a URL opened via Universal Link or custom scheme + /// Resolves campaign links via the Traceback SDK + func handleOpenURL(_ url: URL) async { + print("[URL Received] \(url.absoluteString)") + + // Check if this is a Traceback URL and update the debug flag + let isTraceback = traceback.isTracebackURL(url) + isTracebackURL = isTraceback + + guard isTraceback else { + print("[isTracebackURL] false - ignoring") + debugMessage = "❌ Not a Traceback URL: \(url.host ?? "unknown")" + return + } + + print("[isTracebackURL] true - resolving campaign") + debugMessage = "⏳ Resolving campaign link..." + + do { + let result = try await traceback.campaignSearchLink(url) + + if let result, let deepLink = result.url { + debugMessage = "✅ Campaign link resolved!" + handleCampaignLink(deepLink) + sendAnalytics(result.analytics) + } else { + debugMessage = "ℹ️ No deep link in campaign URL" + } + } catch { + debugMessage = "❌ Campaign resolution failed: \(error.localizedDescription)" + print("[Error] Campaign link resolution failed: \(error)") + } + } +} diff --git a/samples/swiftui-basic/TracebackSwiftUIExample/TracebackSwiftUIExample/Assets.xcassets/AccentColor.colorset/Contents.json b/samples/swiftui-basic/TracebackSwiftUIExample/TracebackSwiftUIExample/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/samples/swiftui-basic/TracebackSwiftUIExample/TracebackSwiftUIExample/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/samples/swiftui-basic/TracebackSwiftUIExample/TracebackSwiftUIExample/Assets.xcassets/AppIcon.appiconset/Contents.json b/samples/swiftui-basic/TracebackSwiftUIExample/TracebackSwiftUIExample/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..13613e3 --- /dev/null +++ b/samples/swiftui-basic/TracebackSwiftUIExample/TracebackSwiftUIExample/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,13 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/samples/swiftui-basic/TracebackSwiftUIExample/TracebackSwiftUIExample/Assets.xcassets/Contents.json b/samples/swiftui-basic/TracebackSwiftUIExample/TracebackSwiftUIExample/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/samples/swiftui-basic/TracebackSwiftUIExample/TracebackSwiftUIExample/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/samples/swiftui-basic/TracebackSwiftUIExample/TracebackSwiftUIExample/ContentView.swift b/samples/swiftui-basic/TracebackSwiftUIExample/TracebackSwiftUIExample/ContentView.swift new file mode 100644 index 0000000..e2b8a59 --- /dev/null +++ b/samples/swiftui-basic/TracebackSwiftUIExample/TracebackSwiftUIExample/ContentView.swift @@ -0,0 +1,214 @@ +// +// ContentView.swift +// TracebackSwiftUIExample +// +// Main view demonstrating Traceback SDK integration +// + +import SwiftUI +import Traceback + +struct ContentView: View { + @EnvironmentObject var appState: AppState + + var body: some View { + ScrollView { + VStack(spacing: 20) { + Text("Debug Status") + .font(.headline) + + // Display debug message directly in body (not in statusSection) for Maestro visibility + Text(appState.debugMessage) + .font(.body) + .foregroundColor(.secondary) + .multilineTextAlignment(.center) + + // Current route display + currentRouteSection + + // Analytics events + analyticsSection + + Spacer() + + // Instructions + instructionsSection + } + .padding() + } + .onAppear { + Task { + await appState.checkPostInstallLink() + } + } + .onOpenURL { url in + Task { + await appState.handleOpenURL(url) + } + } + } + + // MARK: - View Components + + private var statusSection: some View { + VStack(spacing: 8) { + Text(verbatim: appState.debugMessage) + .font(.body) + .foregroundColor(.secondary) + .multilineTextAlignment(.center) + } + .padding() + .frame(maxWidth: .infinity) + .background(Color.blue.opacity(0.1)) + .cornerRadius(10) + } + + private var currentRouteSection: some View { + VStack(spacing: 12) { + Text("SDK Results") + .font(.headline) + + // isTracebackURL result + HStack { + Text("isTracebackURL:") + .font(.caption) + .foregroundColor(.secondary) + Spacer() + if let isTraceback = appState.isTracebackURL { + Text(isTraceback ? "✅ true" : "❌ false") + .font(.caption) + .fontWeight(.semibold) + .foregroundColor(isTraceback ? .green : .red) + } else { + Text("—") + .font(.caption) + .foregroundColor(.gray) + } + } + + Divider() + + // Post-install link result + HStack(alignment: .center, spacing: 4) { + Text("postInstallSearchLink():") + .font(.caption) + .foregroundColor(.secondary) + Spacer() + if let postInstall = appState.postInstallLink { + Text(postInstall.absoluteString) + .font(.caption2) + .foregroundColor(.primary) + .multilineTextAlignment(.leading) + } else { + Text("nil") + .font(.caption2) + .foregroundColor(.gray) + } + } + + Divider() + + // Campaign link result + HStack(alignment: .center, spacing: 4) { + Text("campaignSearchLink():") + .font(.caption) + .foregroundColor(.secondary) + Spacer() + if let campaign = appState.campaignSearchLink { + Text(campaign.absoluteString) + .font(.caption2) + .foregroundColor(.primary) + .multilineTextAlignment(.leading) + } else { + Text("nil") + .font(.caption2) + .foregroundColor(.gray) + } + } + } + .padding() + .frame(maxWidth: .infinity, alignment: .leading) + .background(Color.green.opacity(0.1)) + .cornerRadius(10) + } + + private var analyticsSection: some View { + VStack(alignment: .leading, spacing: 8) { + Text("Analytics Events") + .font(.headline) + + if appState.analyticsEvents.isEmpty { + Text("No events yet") + .font(.caption) + .foregroundColor(.secondary) + } else { + ScrollView { + VStack(alignment: .leading, spacing: 4) { + ForEach(appState.analyticsEvents.indices, id: \.self) { index in + Text("• \(appState.analyticsEvents[index])") + .font(.caption) + .foregroundColor(.secondary) + } + } + } + .frame(maxHeight: 100) + } + } + .padding() + .frame(maxWidth: .infinity, alignment: .leading) + .background(Color.orange.opacity(0.1)) + .cornerRadius(10) + } + + private var instructionsSection: some View { + VStack(alignment: .leading, spacing: 12) { + Text("How to Test") + .font(.headline) + + VStack(alignment: .leading, spacing: 8) { + instructionRow( + icon: "doc.on.clipboard", + title: "Post-Install", + description: "Copy Traceback link, delete app, reinstall" + ) + + instructionRow( + icon: "link", + title: "Campaign", + description: "Tap Traceback link from Messages/Email" + ) + } + } + .padding() + .frame(maxWidth: .infinity, alignment: .leading) + .background(Color.gray.opacity(0.1)) + .cornerRadius(10) + } + + private func instructionRow(icon: String, title: String, description: String) -> some View { + HStack(alignment: .top, spacing: 12) { + Image(systemName: icon) + .foregroundColor(.blue) + .frame(width: 24) + + VStack(alignment: .leading, spacing: 2) { + Text(title) + .font(.subheadline) + .fontWeight(.semibold) + Text(description) + .font(.caption) + .foregroundColor(.secondary) + } + } + } + +} + +// MARK: - Preview + +struct ContentView_Previews: PreviewProvider { + static var previews: some View { + ContentView() + .environmentObject(AppState()) + } +} diff --git a/samples/swiftui-basic/TracebackSwiftUIExample/TracebackSwiftUIExample/Info.plist b/samples/swiftui-basic/TracebackSwiftUIExample/TracebackSwiftUIExample/Info.plist new file mode 100644 index 0000000..0ea26c4 --- /dev/null +++ b/samples/swiftui-basic/TracebackSwiftUIExample/TracebackSwiftUIExample/Info.plist @@ -0,0 +1,35 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + $(MARKETING_VERSION) + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLName + com.example.traceback.swiftui + CFBundleURLSchemes + + myapp + + + + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + + diff --git a/samples/swiftui-basic/TracebackSwiftUIExample/TracebackSwiftUIExample/TracebackSwiftUIExample.entitlements b/samples/swiftui-basic/TracebackSwiftUIExample/TracebackSwiftUIExample/TracebackSwiftUIExample.entitlements new file mode 100644 index 0000000..e6c760c --- /dev/null +++ b/samples/swiftui-basic/TracebackSwiftUIExample/TracebackSwiftUIExample/TracebackSwiftUIExample.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.developer.associated-domains + + applinks:traceback-extension-samples-traceback.web.app + + + diff --git a/samples/swiftui-basic/TracebackSwiftUIExample/TracebackSwiftUIExample/TracebackSwiftUIExampleApp.swift b/samples/swiftui-basic/TracebackSwiftUIExample/TracebackSwiftUIExample/TracebackSwiftUIExampleApp.swift new file mode 100644 index 0000000..b74737c --- /dev/null +++ b/samples/swiftui-basic/TracebackSwiftUIExample/TracebackSwiftUIExample/TracebackSwiftUIExampleApp.swift @@ -0,0 +1,86 @@ +// +// TracebackSwiftUIExampleApp.swift +// TracebackSwiftUIExample +// +// A basic SwiftUI app demonstrating Traceback SDK integration +// + +import SwiftUI +import Traceback + +@main +struct TracebackSwiftUIExampleApp: App { + @StateObject private var appState = AppState() + + var body: some Scene { + WindowGroup { + ContentView() + .environmentObject(appState) + } + } +} + +// MARK: - App State + +@MainActor +class AppState: ObservableObject { + // SDK Debug Information + @Published var postInstallLink: URL? + @Published var campaignSearchLink: URL? + @Published var isTracebackURL: Bool? + @Published var debugMessage: String = "Waiting for links..." + @Published var analyticsEvents: [String] = [] + + lazy var traceback: TracebackSDK = { + let config = TracebackConfiguration( + mainAssociatedHost: URL(string: "https://traceback-extension-samples-traceback.web.app")!, + useClipboard: true, + logLevel: .debug + ) + return TracebackSDK.live(config: config) + }() + + init() { + // Run diagnostics on init (only in debug builds) + #if DEBUG + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in + self?.traceback.performDiagnostics() + } + #endif + } + + func handlePostInstallLink(_ url: URL) { + postInstallLink = url + debugMessage = "Post-install link: \(url.absoluteString)" + print("[Post-Install] \(url.absoluteString)") + } + + func handleCampaignLink(_ url: URL) { + campaignSearchLink = url + debugMessage = "Campaign link: \(url.absoluteString)" + print("[Campaign] \(url.absoluteString)") + } + + func sendAnalytics(_ events: [TracebackAnalyticsEvent]) { + for event in events { + let eventDescription = formatAnalyticsEvent(event) + analyticsEvents.append(eventDescription) + print("[Analytics] \(eventDescription)") + } + } + + private func formatAnalyticsEvent(_ event: TracebackAnalyticsEvent) -> String { + switch event { + case .postInstallDetected(let url): + return "Post-install detected: \(url.absoluteString)" + case .postInstallError(let error): + return "Post-install error: \(error.localizedDescription)" + case .campaignResolved(let url): + return "Campaign resolved: \(url.absoluteString)" + case .campaignResolvedLocally(let url): + return "Campaign resolved locally: \(url.absoluteString)" + case .campaignError(let error): + return "Campaign error: \(error.localizedDescription)" + } + } +}