Skip to main content

Client Implementation Approaches

This guide covers how to implement ATP clients across different platforms and environments, including mobile apps, web applications, desktop software, and command-line tools.

Mobile Implementations

Native Mobile Integration

Integrating ATP notifications into mobile applications provides a seamless experience for end users.

Setting Up the Client

  • Android (Kotlin)
  • iOS (Swift)
  • Flutter
  • React Native
// Add to your app-level build.gradle
dependencies {
    implementation("io.atp:android-client:1.0.0")
}

// Initialize client in your Application class
class MyApplication : Application() {
    lateinit var atpClient: ATPClient
    
    override fun onCreate() {
        super.onCreate()
        
        atpClient = ATPClient.Builder(this)
            .apiKey("your_client_api_key")
            .endpoint("https://api.atp.example.com")
            .build()
    }
}

Setting Up Push Notifications

To receive notifications in real-time, configure push notifications:
  • Android (Kotlin)
  • iOS (Swift)
  • React Native
// In your application class
override fun onCreate() {
    super.onCreate()
    
    // Initialize Firebase Messaging
    FirebaseMessaging.getInstance().token.addOnCompleteListener { task ->
        if (task.isSuccessful) {
            val token = task.result
            // Register FCM token with ATP
            atpClient.registerPushToken(token, "fcm")
        }
    }
}

// Create a Firebase Messaging Service
class ATPMessagingService : FirebaseMessagingService() {
    override fun onMessageReceived(remoteMessage: RemoteMessage) {
        // Check if it's an ATP notification
        if (remoteMessage.data.containsKey("atp_notification_id")) {
            val notificationId = remoteMessage.data["atp_notification_id"]
            val app = applicationContext as MyApplication
            
            // Fetch the full notification
            app.atpClient.getNotification(notificationId) { notification ->
                // Display the notification
                showNotification(notification)
            }
        }
    }
}

Displaying Notifications

Creating a custom UI for ATP notifications:
  • Android (Kotlin)
  • iOS (Swift)
  • React Native
// Create a notification view
class NotificationActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_notification)
        
        // Get notification ID from intent
        val notificationId = intent.getStringExtra("notification_id") ?: return
        
        // Get the application instance
        val app = application as MyApplication
        
        // Fetch notification details
        app.atpClient.getNotification(notificationId) { notification ->
            // Update UI
            findViewById<TextView>(R.id.notification_title).text = notification.title
            findViewById<TextView>(R.id.notification_description).text = notification.description
            
            // Create action buttons dynamically
            val actionsContainer = findViewById<LinearLayout>(R.id.actions_container)
            
            notification.actions.forEach { action ->
                val button = Button(this)
                button.text = action.label
                button.setOnClickListener {
                    handleAction(notificationId, action)
                }
                actionsContainer.addView(button)
            }
        }
    }
    
    private fun handleAction(notificationId: String, action: ATPAction) {
        val app = application as MyApplication
        
        when (action.responseType) {
            "simple" -> {
                // Simple action - just submit the response
                app.atpClient.respondToNotification(
                    notificationId = notificationId,
                    actionId = action.id,
                    responseData = null
                )
            }
            "text" -> {
                // Show text input dialog
                val editText = EditText(this)
                AlertDialog.Builder(this)
                    .setTitle(action.label)
                    .setView(editText)
                    .setPositiveButton("Submit") { _, _ ->
                        app.atpClient.respondToNotification(
                            notificationId = notificationId,
                            actionId = action.id,
                            responseData = editText.text.toString()
                        )
                    }
                    .setNegativeButton("Cancel", null)
                    .show()
            }
            // Handle other response types...
        }
    }
}

Background Processing

For handling notifications when the app is in the background:
  • Android (Kotlin)
  • iOS (Swift)
// Create a WorkManager task
class NotificationWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {
    override suspend fun doWork(): Result {
        val notificationId = inputData.getString("notification_id") ?: return Result.failure()
        
        // Get the application instance
        val app = applicationContext as MyApplication
        
        // Process the notification
        return try {
            val notification = app.atpClient.getNotificationSync(notificationId)
            
            // Create and show a system notification
            val builder = NotificationCompat.Builder(applicationContext, CHANNEL_ID)
                .setSmallIcon(R.drawable.ic_notification)
                .setContentTitle(notification.title)
                .setContentText(notification.description)
                .setPriority(NotificationCompat.PRIORITY_DEFAULT)
            
            // Create an intent to open the notification detail activity
            val intent = Intent(applicationContext, NotificationActivity::class.java).apply {
                putExtra("notification_id", notificationId)
            }
            val pendingIntent = PendingIntent.getActivity(
                applicationContext, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT
            )
            builder.setContentIntent(pendingIntent)
            
            // Show the notification
            val notificationManager = NotificationManagerCompat.from(applicationContext)
            notificationManager.notify(notificationId.hashCode(), builder.build())
            
            Result.success()
        } catch (e: Exception) {
            Result.retry()
        }
    }
}

Web Implementations

Browser-Based Clients

Implementing ATP in web applications:
  • JavaScript
  • React
  • Vue
// Install with npm or yarn
// npm install @atp/browser-client

import { ATPClient } from '@atp/browser-client';

// Initialize the client
const atpClient = new ATPClient({
  apiKey: 'your_client_api_key',
  endpoint: 'https://api.atp.example.com',
  userId: 'current_user_id' // If available
});

// Set up notification polling or WebSocket connection
atpClient.connectToNotificationStream({
  onNotification: (notification) => {
    // Display the notification
    showNotification(notification);
  },
  onError: (error) => {
    console.error('ATP connection error:', error);
  }
});

// Display notification function
function showNotification(notification) {
  // Create notification UI
  const notificationElement = document.createElement('div');
  notificationElement.className = 'atp-notification';
  
  // Add title and description
  notificationElement.innerHTML = `
    <div class="notification-header">
      <h3>${notification.title}</h3>
      <button class="close-btn">×</button>
    </div>
    <p>${notification.description}</p>
    <div class="notification-actions"></div>
  `;
  
  // Add action buttons
  const actionsContainer = notificationElement.querySelector('.notification-actions');
  
  notification.actions.forEach(action => {
    const button = document.createElement('button');
    button.textContent = action.label;
    button.addEventListener('click', () => {
      handleAction(notification.id, action);
    });
    
    actionsContainer.appendChild(button);
  });
  
  // Add close button handler
  const closeBtn = notificationElement.querySelector('.close-btn');
  closeBtn.addEventListener('click', () => {
    notificationElement.remove();
  });
  
  // Add to notification container
  document.getElementById('notification-container').appendChild(notificationElement);
}

// Handle notification actions
function handleAction(notificationId, action) {
  switch (action.response_type) {
    case 'simple':
      // Simple action - just submit the response
      atpClient.respondToNotification(notificationId, action.id)
        .then(() => {
          // Remove the notification
          document.querySelector(`[data-notification-id="${notificationId}"]`).remove();
        })
        .catch(error => {
          console.error('Failed to submit response:', error);
        });
      break;
      
    case 'text':
      // Show text input dialog
      const textInput = prompt(action.response_options?.placeholder || 'Enter your response:');
      
      if (textInput !== null) {
        atpClient.respondToNotification(notificationId, action.id, textInput)
          .then(() => {
            // Remove the notification
            document.querySelector(`[data-notification-id="${notificationId}"]`).remove();
          })
          .catch(error => {
            console.error('Failed to submit response:', error);
          });
      }
      break;
      
    // Handle other response types...
  }
}

Web Push Notifications

Implementing browser push notifications for ATP:
  • JavaScript
// Request push notification permission
async function registerForPushNotifications() {
  // Check if the browser supports service workers and push notifications
  if ('serviceWorker' in navigator && 'PushManager' in window) {
    try {
      // Register service worker
      const registration = await navigator.serviceWorker.register('/service-worker.js');
      
      // Request permission
      const permission = await Notification.requestPermission();
      if (permission !== 'granted') {
        throw new Error('Permission not granted for notifications');
      }
      
      // Get push subscription
      const subscription = await registration.pushManager.subscribe({
        userVisibleOnly: true,
        applicationServerKey: urlBase64ToUint8Array('your_vapid_public_key')
      });
      
      // Register the subscription with ATP
      const atpClient = new ATPClient({
        apiKey: 'your_client_api_key',
        endpoint: 'https://api.atp.example.com'
      });
      
      await atpClient.registerPushSubscription(subscription.toJSON());
      
      return {
        success: true,
        subscription
      };
    } catch (error) {
      console.error('Failed to register for push notifications:', error);
      return {
        success: false,
        error
      };
    }
  } else {
    return {
      success: false,
      error: new Error('Push notifications not supported')
    };
  }
}

// Helper function to convert base64 to Uint8Array
function urlBase64ToUint8Array(base64String) {
  const padding = '='.repeat((4 - base64String.length % 4) % 4);
  const base64 = (base64String + padding)
    .replace(/-/g, '+')
    .replace(/_/g, '/');
  
  const rawData = window.atob(base64);
  const outputArray = new Uint8Array(rawData.length);
  
  for (let i = 0; i < rawData.length; ++i) {
    outputArray[i] = rawData.charCodeAt(i);
  }
  return outputArray;
}
Service Worker Implementation:
  • JavaScript (service-worker.js)
// service-worker.js
self.addEventListener('push', function(event) {
  // Parse the notification data
  const data = event.data.json();
  
  // Check if it's an ATP notification
  if (data.type === 'atp_notification') {
    // Extract notification details
    const { notification_id, title, body } = data;
    
    // Show the notification
    const showNotification = self.registration.showNotification(title, {
      body,
      icon: '/icon.png',
      badge: '/badge.png',
      data: {
        atp_notification_id: notification_id
      },
      actions: [
        {
          action: 'open',
          title: 'View Details'
        }
      ]
    });
    
    event.waitUntil(showNotification);
  }
});

self.addEventListener('notificationclick', function(event) {
  event.notification.close();
  
  // Get the notification data
  const notificationData = event.notification.data;
  
  if (notificationData && notificationData.atp_notification_id) {
    // Open a window to show the notification details
    const url = `/notifications/${notificationData.atp_notification_id}`;
    
    event.waitUntil(
      clients.matchAll({ type: 'window' }).then(windowClients => {
        // Check if there is already a window/tab open with the target URL
        for (let i = 0; i < windowClients.length; i++) {
          const client = windowClients[i];
          if (client.url === url && 'focus' in client) {
            return client.focus();
          }
        }
        
        // If no window/tab is open, open a new one
        if (clients.openWindow) {
          return clients.openWindow(url);
        }
      })
    );
  }
});

Desktop Application Integration

Electron Applications

Implementing ATP in Electron desktop applications:
  • JavaScript (Electron)
// main.js
const { app, BrowserWindow, ipcMain, Notification } = require('electron');
const { ATPClient } = require('@atp/node-client');

let mainWindow;
let atpClient;

// Initialize ATP client
function initializeATP() {
  atpClient = new ATPClient({
    apiKey: 'your_client_api_key',
    endpoint: 'https://api.atp.example.com'
  });
  
  // Set up notification stream
  atpClient.connectToNotificationStream({
    onNotification: (notification) => {
      // Show system notification
      showSystemNotification(notification);
      
      // Send to renderer process
      if (mainWindow && !mainWindow.isDestroyed()) {
        mainWindow.webContents.send('atp-notification', notification);
      }
    },
    onError: (error) => {
      console.error('ATP connection error:', error);
    }
  });
}

function showSystemNotification(notification) {
  // Check if Electron notifications are supported
  if (!Notification.isSupported()) {
    return;
  }
  
  const systemNotification = new Notification({
    title: notification.title,
    body: notification.description,
    icon: './app-icon.png'
  });
  
  systemNotification.on('click', () => {
    // Focus the app and show the notification
    if (mainWindow && !mainWindow.isDestroyed()) {
      if (mainWindow.isMinimized()) {
        mainWindow.restore();
      }
      mainWindow.focus();
      
      // Show notification details
      mainWindow.webContents.send('show-notification-details', notification);
    }
  });
  
  systemNotification.show();
}

app.on('ready', () => {
  // Create main window
  mainWindow = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      nodeIntegration: true,
      contextIsolation: false
    }
  });
  
  mainWindow.loadFile('index.html');
  
  // Initialize ATP after the app is ready
  initializeATP();
});

// Handle response submission from renderer
ipcMain.on('submit-notification-response', (event, data) => {
  const { notificationId, actionId, responseData } = data;
  
  atpClient.respondToNotification(notificationId, actionId, responseData)
    .then(result => {
      event.reply('response-submitted', { success: true, notificationId });
    })
    .catch(error => {
      console.error('Failed to submit response:', error);
      event.reply('response-submitted', { 
        success: false, 
        notificationId,
        error: error.message 
      });
    });
});
Renderer Process Implementation:
  • JavaScript (Electron Renderer)
// renderer.js
const { ipcRenderer } = require('electron');

// Notification container element
const notificationContainer = document.getElementById('notification-container');

// Listen for ATP notifications
ipcRenderer.on('atp-notification', (event, notification) => {
  // Create notification element
  const notificationElement = createNotificationElement(notification);
  
  // Add to container
  notificationContainer.appendChild(notificationElement);
});

// Listen for show notification details request
ipcRenderer.on('show-notification-details', (event, notification) => {
  // Focus or create notification element
  let notificationElement = document.querySelector(`[data-notification-id="${notification.id}"]`);
  
  if (!notificationElement) {
    notificationElement = createNotificationElement(notification);
    notificationContainer.appendChild(notificationElement);
  }
  
  // Scroll to element and highlight it
  notificationElement.scrollIntoView({ behavior: 'smooth' });
  notificationElement.classList.add('highlight');
  
  // Remove highlight after animation
  setTimeout(() => {
    notificationElement.classList.remove('highlight');
  }, 2000);
});

// Create notification element
function createNotificationElement(notification) {
  const element = document.createElement('div');
  element.className = 'atp-notification';
  element.dataset.notificationId = notification.id;
  
  // Add title and description
  element.innerHTML = `
    <div class="notification-header">
      <h3>${notification.title}</h3>
      <button class="close-btn">×</button>
    </div>
    <p>${notification.description}</p>
    <div class="notification-actions"></div>
  `;
  
  // Add action buttons
  const actionsContainer = element.querySelector('.notification-actions');
  
  notification.actions.forEach(action => {
    const actionElement = createActionElement(notification.id, action);
    actionsContainer.appendChild(actionElement);
  });
  
  // Add close button handler
  const closeBtn = element.querySelector('.close-btn');
  closeBtn.addEventListener('click', () => {
    element.remove();
  });
  
  return element;
}

// Create action element based on response type
function createActionElement(notificationId, action) {
  const container = document.createElement('div');
  container.className = 'action-container';
  
  switch (action.response_type) {
    case 'simple':
      const button = document.createElement('button');
      button.textContent = action.label;
      button.addEventListener('click', () => {
        submitResponse(notificationId, action.id);
      });
      container.appendChild(button);
      break;
      
    case 'text':
      const input = document.createElement('input');
      input.type = 'text';
      input.placeholder = action.response_options?.placeholder || 'Enter your response';
      
      const submitBtn = document.createElement('button');
      submitBtn.textContent = action.label;
      submitBtn.addEventListener('click', () => {
        submitResponse(notificationId, action.id, input.value);
      });
      
      container.appendChild(input);
      container.appendChild(submitBtn);
      break;
      
    // Handle other response types...
  }
  
  return container;
}

// Submit response to main process
function submitResponse(notificationId, actionId, responseData = null) {
  ipcRenderer.send('submit-notification-response', {
    notificationId,
    actionId,
    responseData
  });
}

// Handle response submission result
ipcRenderer.on('response-submitted', (event, result) => {
  if (result.success) {
    // Remove the notification element
    const notificationElement = document.querySelector(
      `[data-notification-id="${result.notificationId}"]`
    );
    
    if (notificationElement) {
      notificationElement.remove();
    }
  } else {
    // Show error message
    alert(`Failed to submit response: ${result.error}`);
  }
});

CLI Integration

Command Line Tools

Implementing ATP in command-line applications:
  • Node.js CLI
  • Python CLI
#!/usr/bin/env node
const { ATPClient } = require('@atp/node-client');
const inquirer = require('inquirer');
const notifier = require('node-notifier');
const path = require('path');
const fs = require('fs');
const os = require('os');

// Configuration file path
const configPath = path.join(os.homedir(), '.atpcli.json');

// Load or create configuration
function loadConfig() {
  try {
    if (fs.existsSync(configPath)) {
      return JSON.parse(fs.readFileSync(configPath, 'utf8'));
    }
  } catch (error) {
    console.error('Error loading config:', error.message);
  }
  
  return { apiKey: '', endpoint: 'https://api.atp.example.com' };
}

// Save configuration
function saveConfig(config) {
  try {
    fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
  } catch (error) {
    console.error('Error saving config:', error.message);
  }
}

// Initialize ATP client
async function initializeClient() {
  let config = loadConfig();
  
  // If no API key, prompt for it
  if (!config.apiKey) {
    const answers = await inquirer.prompt([
      {
        type: 'input',
        name: 'apiKey',
        message: 'Enter your ATP API key:',
        validate: input => input.trim() !== '' ? true : 'API key is required'
      }
    ]);
    
    config.apiKey = answers.apiKey;
    saveConfig(config);
  }
  
  return new ATPClient({
    apiKey: config.apiKey,
    endpoint: config.endpoint
  });
}

// Display a desktop notification
function showDesktopNotification(notification) {
  notifier.notify({
    title: notification.title,
    message: notification.description,
    icon: path.join(__dirname, 'icon.png'),
    sound: true,
    wait: true
  });
  
  notifier.on('click', () => {
    // Show notification details and prompt for response
    displayNotificationDetails(notification);
  });
}

// Display notification details in console
async function displayNotificationDetails(notification) {
  console.log('\n-----------------------------------');
  console.log(`Notification: ${notification.title}`);
  console.log('-----------------------------------');
  console.log(notification.description);
  console.log('-----------------------------------');
  
  // Create action choices for inquirer
  const choices = notification.actions.map(action => ({
    name: action.label,
    value: action
  }));
  
  // Prompt user to select an action
  const { selectedAction } = await inquirer.prompt([
    {
      type: 'list',
      name: 'selectedAction',
      message: 'Select an action:',
      choices
    }
  ]);
  
  // Handle the selected action
  await handleAction(notification.id, selectedAction);
}

// Handle action based on response type
async function handleAction(notificationId, action) {
  const client = await initializeClient();
  
  switch (action.response_type) {
    case 'simple':
      // Simple action - just submit the response
      await client.respondToNotification(notificationId, action.id);
      console.log('Response submitted successfully!');
      break;
      
    case 'text':
      // Prompt for text input
      const { textResponse } = await inquirer.prompt([
        {
          type: 'input',
          name: 'textResponse',
          message: action.response_options?.placeholder || 'Enter your response:',
          validate: input => {
            if (action.response_options?.required && input.trim() === '') {
              return 'Response is required';
            }
            return true;
          }
        }
      ]);
      
      await client.respondToNotification(notificationId, action.id, textResponse);
      console.log('Response submitted successfully!');
      break;
      
    case 'select':
      // Prompt for selection
      const choices = action.response_options?.options.map(option => ({
        name: option.label,
        value: option.value
      }));
      
      const { selectedOption } = await inquirer.prompt([
        {
          type: 'list',
          name: 'selectedOption',
          message: 'Select an option:',
          choices
        }
      ]);
      
      await client.respondToNotification(notificationId, action.id, selectedOption);
      console.log('Response submitted successfully!');
      break;
      
    // Handle other response types...
    default:
      console.log(`Response type '${action.response_type}' is not supported in CLI mode`);
  }
}

// Main function
async function main() {
  try {
    const client = await initializeClient();
    
    // Parse command line arguments
    const args = process.argv.slice(2);
    const command = args[0];
    
    if (command === 'listen') {
      // Start listening for notifications
      console.log('Listening for ATP notifications. Press Ctrl+C to exit.');
      
      client.connectToNotificationStream({
        onNotification: notification => {
          showDesktopNotification(notification);
          console.log(`\nNew notification: ${notification.title}`);
        },
        onError: error => {
          console.error('ATP connection error:', error);
        }
      });
      
      // Keep the process running
      process.stdin.resume();
    } else if (command === 'list') {
      // List recent notifications
      const notifications = await client.getRecentNotifications();
      
      if (notifications.length === 0) {
        console.log('No recent notifications.');
        return;
      }
      
      console.log('\nRecent notifications:');
      notifications.forEach((notification, index) => {
        console.log(`${index + 1}. ${notification.title} (${notification.id})`);
      });
      
      // Prompt to select a notification
      const { selectedIndex } = await inquirer.prompt([
        {
          type: 'list',
          name: 'selectedIndex',
          message: 'Select a notification to view:',
          choices: notifications.map((n, i) => ({
            name: n.title,
            value: i
          }))
        }
      ]);
      
      displayNotificationDetails(notifications[selectedIndex]);
    } else {
      // Show help
      console.log('Usage:');
      console.log('  atp-cli listen         Listen for new notifications');
      console.log('  atp-cli list           List recent notifications');
    }
  } catch (error) {
    console.error('Error:', error.message);
    process.exit(1);
  }
}

// Run the main function
main();

Verification Examples

Let’s verify that our implementations work correctly with these test examples:

Mobile Verification

  • Android
  • iOS
// Add this function to your activity or fragment for testing
fun verifyATPImplementation() {
    val app = application as MyApplication
    
    // 1. Test client initialization
    if (app.atpClient == null) {
        Log.e("ATPVerification", "ATP client is not initialized")
        return
    }
    
    // 2. Test FCM token registration
    FirebaseMessaging.getInstance().token.addOnCompleteListener { task ->
        if (task.isSuccessful) {
            val token = task.result
            app.atpClient.registerPushToken(token, "fcm", { success ->
                Log.d("ATPVerification", "FCM token registration: ${if (success) "SUCCESS" else "FAILED"}")
            })
        } else {
            Log.e("ATPVerification", "Failed to get FCM token")
        }
    }
    
    // 3. Test notification rendering with a mock notification
    val mockNotification = ATPNotification(
        id = "test_notification_id",
        title = "Test Notification",
        description = "This is a test notification to verify the ATP implementation",
        priority = "normal",
        actions = listOf(
            ATPAction(
                id = "test_action",
                label = "Test Action",
                responseType = "simple"
            ),
            ATPAction(
                id = "text_action",
                label = "Test Text Response",
                responseType = "text",
                responseOptions = mapOf(
                    "placeholder" to "Enter test response"
                )
            )
        )
    )
    
    // Display the mock notification to verify UI
    showNotification(mockNotification)
}

Web Verification

  • JavaScript
// Add this function to your web app for testing
function verifyATPImplementation() {
  console.log('Starting ATP verification...');
  
  // 1. Verify client initialization
  if (!window.atpClient) {
    console.error('ATP client is not initialized');
    return;
  }
  
  console.log('ATP client initialized successfully');
  
  // 2. Test notification stream connection
  try {
    const connection = window.atpClient.connectToNotificationStream({
      onNotification: (notification) => {
        console.log('Test notification stream works!', notification);
        // Disconnect after successful test
        connection.disconnect();
      },
      onError: (error) => {
        console.error('ATP connection error:', error);
      }
    });
    
    console.log('Notification stream connection established');
  } catch (error) {
    console.error('Failed to connect to notification stream:', error);
  }
  
  // 3. Test notification rendering with a mock notification
  const mockNotification = {
    id: 'test_notification_id',
    title: 'Test Notification',
    description: 'This is a test notification to verify the ATP implementation',
    priority: 'normal',
    actions: [
      {
        id: 'test_action',
        label: 'Test Action',
        response_type: 'simple'
      },
      {
        id: 'text_action',
        label: 'Test Text Response',
        response_type: 'text',
        response_options: {
          placeholder: 'Enter test response'
        }
      }
    ]
  };
  
  // Display the mock notification to verify UI
  showNotification(mockNotification);
  
  console.log('Mock notification displayed for verification');
  
  // 4. Test response submission with a mock response
  setTimeout(() => {
    console.log('Testing response submission...');
    
    window.atpClient.respondToNotification('test_notification_id', 'test_action')
      .then(() => {
        console.log('Test response submission successful!');
      })
      .catch(error => {
        console.error('Test response submission failed:', error);
      });
  }, 5000);
}

Verification: Knowledge Check

Before proceeding, let’s verify your understanding of ATP client implementation:

Next Steps

Now that you’ve learned how to implement ATP clients across different platforms, explore these additional resources:

Best Practices

Learn recommended patterns and practices for robust ATP implementations

Troubleshooting

Common issues and solutions for ATP client implementations