Documentation Index
Fetch the complete documentation index at: https://atp.hypertext.studio/llms.txt
Use this file to discover all available pages before exploring further.
Common Issues and Solutions
This guide provides solutions for common issues you might encounter when implementing ATP in your applications.Connection Issues
Unable to Connect to ATP Server
- Symptoms
- Causes
- Solutions
- Connection timeout errors
- Network-related exceptions
- “Cannot reach server” errors
- Invalid API endpoint
- Network connectivity issues
- Firewall blocking outbound connections
- Incorrect SSL/TLS configuration
// Check your endpoint configuration
// Ensure you're using the correct endpoint for your environment
// Development environment
const client = new ATPClient({
apiKey: 'your_test_api_key',
endpoint: 'https://api.test.atp.example.com',
environment: 'development'
});
// Production environment
const client = new ATPClient({
apiKey: 'your_live_api_key',
endpoint: 'https://api.atp.example.com',
environment: 'production'
});
// Test connectivity with a ping
async function testConnection() {
try {
await client.ping();
console.log('Connection successful!');
return true;
} catch (error) {
console.error('Connection failed:', error.message);
// Check for specific error types
if (error.code === 'ECONNREFUSED') {
console.error('Server could not be reached. Check your network connection.');
} else if (error.code === 'ETIMEDOUT') {
console.error('Connection timed out. Check your firewall settings.');
} else if (error.code === 'CERT_HAS_EXPIRED') {
console.error('SSL certificate issue. Check your TLS configuration.');
}
return false;
}
}
WebSocket Connection Failures
- Symptoms
- Causes
- Solutions
- WebSocket connection errors
- Frequent disconnections
- Notification delays or missing notifications
- Firewall blocking WebSocket traffic
- Proxy servers not configured for WebSockets
- Client-side network changes (switching Wi-Fi, etc.)
// Implement robust connection handling with retries
class RobustWebSocketManager {
constructor(atpClient, options = {}) {
this.atpClient = atpClient;
this.connection = null;
this.backoffTime = options.initialBackoff || 1000; // Start with 1 second
this.maxBackoff = options.maxBackoff || 30000; // Max 30 seconds
this.reconnectAttempts = 0;
this.maxReconnectAttempts = options.maxReconnectAttempts || 10;
this.listeners = {
onNotification: options.onNotification || (() => {}),
onConnected: options.onConnected || (() => {}),
onDisconnected: options.onDisconnected || (() => {}),
onError: options.onError || (() => {})
};
// Auto-reconnect on network status change
if (typeof window !== 'undefined' && window.addEventListener) {
window.addEventListener('online', () => this.reconnect());
}
}
connect() {
if (this.connection) {
return; // Already connected
}
try {
this.connection = this.atpClient.connectToNotificationStream({
onNotification: (notification) => {
this.listeners.onNotification(notification);
},
onError: (error) => {
this.listeners.onError(error);
this.handleConnectionFailure(error);
},
onClose: () => {
this.listeners.onDisconnected();
this.connection = null;
this.reconnect();
}
});
// Connection successful
this.backoffTime = 1000; // Reset backoff
this.reconnectAttempts = 0;
this.listeners.onConnected();
} catch (error) {
this.listeners.onError(error);
this.handleConnectionFailure(error);
}
}
handleConnectionFailure(error) {
console.error('WebSocket connection failed:', error);
this.connection = null;
// Check for specific WebSocket errors
if (error.code === 1006) {
console.error('Abnormal closure. Possible network issue.');
} else if (error.code === 1008) {
console.error('Policy violation. Check your API permissions.');
} else if (error.code === 1011) {
console.error('Server error. Check server status.');
}
this.reconnect();
}
reconnect() {
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
console.error('Maximum reconnection attempts reached. Giving up.');
this.listeners.onError(new Error('Maximum reconnection attempts reached'));
return;
}
// Implement exponential backoff
const delay = Math.min(
this.backoffTime * Math.pow(1.5, this.reconnectAttempts),
this.maxBackoff
);
console.log(`Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts + 1})`);
setTimeout(() => {
this.reconnectAttempts++;
this.connect();
}, delay);
}
disconnect() {
if (this.connection) {
this.connection.disconnect();
this.connection = null;
}
}
}
// Usage
const wsManager = new RobustWebSocketManager(atpClient, {
onNotification: (notification) => {
console.log('Received notification:', notification.title);
// Process notification...
},
onConnected: () => {
console.log('WebSocket connected successfully');
// Update UI to show connected status
},
onDisconnected: () => {
console.log('WebSocket disconnected');
// Update UI to show disconnected status
},
onError: (error) => {
console.error('WebSocket error:', error);
// Display error to user
}
});
wsManager.connect();
Authentication Issues
API Key Authentication Failures
- Symptoms
- Causes
- Solutions
- 401 Unauthorized errors
- “Invalid API key” error messages
- Authentication failures when sending notifications
- Incorrect API key
- Expired API key
- Using development key in production or vice versa
- API key has insufficient permissions
// Verify your API key is correct and has the right permissions
async function verifyAPIKey() {
try {
const client = new ATPClient({
apiKey: 'your_api_key',
endpoint: 'https://api.atp.example.com'
});
// Try to get account info (a lightweight operation)
const accountInfo = await client.getAccountInfo();
console.log('API key is valid. Account:', accountInfo.name);
console.log('Permissions:', accountInfo.permissions);
console.log('Environment:', accountInfo.environment);
// Check if it's a development key in production or vice versa
if (accountInfo.environment === 'development' &&
process.env.NODE_ENV === 'production') {
console.warn('WARNING: Using development API key in production environment!');
}
return true;
} catch (error) {
console.error('API key verification failed:', error.message);
if (error.status === 401) {
console.error('Invalid API key. Please check your key.');
} else if (error.message.includes('permissions')) {
console.error('API key has insufficient permissions.');
} else if (error.message.includes('expired')) {
console.error('API key has expired.');
}
return false;
}
}
// Implementation of key rotation
class RotatingKeyManager {
constructor(keys, options = {}) {
this.keys = Array.isArray(keys) ? keys : [keys];
this.currentKeyIndex = 0;
this.lastRotation = Date.now();
this.rotationInterval = options.rotationInterval || (24 * 60 * 60 * 1000); // 24 hours
this.onKeyFailure = options.onKeyFailure || (() => {});
}
getCurrentKey() {
// Check if it's time to rotate
if (Date.now() - this.lastRotation > this.rotationInterval) {
this.rotate();
}
return this.keys[this.currentKeyIndex];
}
rotate() {
this.currentKeyIndex = (this.currentKeyIndex + 1) % this.keys.length;
this.lastRotation = Date.now();
}
handleKeyFailure(key, error) {
// Remove the failed key if we have others
if (this.keys.length > 1) {
this.keys = this.keys.filter(k => k !== key);
this.currentKeyIndex = 0; // Reset to first key
}
this.onKeyFailure(key, error);
}
}
// Usage
const keyManager = new RotatingKeyManager([
'primary_api_key',
'backup_api_key'
], {
onKeyFailure: (key, error) => {
// Send alert to administrators
console.error(`API key ${key.substring(0, 8)}... failed:`, error);
alertAdmins(`API key failure: ${error.message}`);
}
});
// Create client with key manager
const client = new ATPClient({
apiKey: keyManager.getCurrentKey(),
endpoint: 'https://api.atp.example.com'
});
// Handle key failure in API calls
async function sendNotificationWithKeyFailureHandling(notification) {
try {
return await client.sendNotification(notification);
} catch (error) {
if (error.status === 401) {
const currentKey = keyManager.getCurrentKey();
keyManager.handleKeyFailure(currentKey, error);
// Retry with a new key
client.setApiKey(keyManager.getCurrentKey());
return await client.sendNotification(notification);
}
throw error;
}
}
Webhook Signature Verification Failures
- Symptoms
- Causes
- Solutions
- Webhook verification errors
- “Invalid signature” messages in logs
- Missing webhook responses
- Incorrect webhook secret
- Mismatched signature computation algorithm
- Request body modification by middleware
- Non-timing-safe comparison method
// Ensure your signature verification is correct
function verifyWebhookSignature(payload, signature, secret) {
// Convert payload to string if it's an object
const payloadString = typeof payload === 'string'
? payload
: JSON.stringify(payload);
// Create a HMAC SHA-256 hash using the secret
const computedSignature = crypto
.createHmac('sha256', secret)
.update(payloadString)
.digest('hex');
// Use timing-safe comparison to prevent timing attacks
return crypto.timingSafeEqual(
Buffer.from(computedSignature),
Buffer.from(signature)
);
}
// Express middleware to preserve raw body for signature verification
function preserveRawBody(req, res, next) {
let data = '';
req.on('data', chunk => {
data += chunk;
});
req.on('end', () => {
req.rawBody = data;
next();
});
}
// Express webhook handler with proper verification
app.post('/webhook', preserveRawBody, (req, res) => {
const signature = req.headers['x-atp-signature'];
const webhookSecret = process.env.WEBHOOK_SECRET;
if (!signature) {
console.error('Missing signature header');
return res.status(401).json({ error: 'Missing signature' });
}
try {
// Use the raw body for verification
const isValid = verifyWebhookSignature(req.rawBody, signature, webhookSecret);
if (!isValid) {
console.error('Invalid webhook signature');
return res.status(401).json({ error: 'Invalid signature' });
}
// Parse the body if needed
const payload = typeof req.body === 'object' ? req.body : JSON.parse(req.rawBody);
// Process webhook
processWebhook(payload);
res.status(200).json({ status: 'success' });
} catch (error) {
console.error('Webhook verification error:', error);
res.status(500).json({ error: 'Webhook processing error' });
}
});
// Debug webhook issues
function debugWebhook(req) {
const signature = req.headers['x-atp-signature'];
const webhookSecret = process.env.WEBHOOK_SECRET;
console.log('Received webhook:');
console.log('Headers:', JSON.stringify(req.headers, null, 2));
console.log('Raw body length:', req.rawBody?.length);
console.log('Parsed body:', JSON.stringify(req.body, null, 2));
// Try multiple signature methods to debug issues
const hmacSha256 = crypto
.createHmac('sha256', webhookSecret)
.update(req.rawBody)
.digest('hex');
const hmacSha256Json = crypto
.createHmac('sha256', webhookSecret)
.update(JSON.stringify(req.body))
.digest('hex');
const hmacSha256SortedJson = crypto
.createHmac('sha256', webhookSecret)
.update(JSON.stringify(req.body, Object.keys(req.body).sort()))
.digest('hex');
console.log('Expected signature (raw):', hmacSha256);
console.log('Expected signature (JSON):', hmacSha256Json);
console.log('Expected signature (sorted JSON):', hmacSha256SortedJson);
console.log('Actual signature:', signature);
}
Notification Issues
Notifications Not Being Received
- Symptoms
- Causes
- Solutions
- Notifications are sent but never received by clients
- Missing notifications in client applications
- Notification count discrepancies
- Incorrect client configuration
- Connection issues between client and ATP server
- Push notification registration issues
- Expired notification tokens
// Verify notification delivery
async function verifyNotificationDelivery(notificationId) {
try {
// Check notification status
const status = await client.getNotificationStatus(notificationId);
console.log('Notification status:', status.status);
if (status.status === 'delivered') {
console.log('Notification was delivered successfully at:', status.delivered_at);
} else if (status.status === 'pending') {
console.log('Notification is still pending delivery');
} else if (status.status === 'failed') {
console.error('Notification delivery failed:', status.error);
}
// Check client registration
const clients = await client.getRegisteredClients();
console.log(`Found ${clients.length} registered clients`);
if (clients.length === 0) {
console.error('No clients registered to receive notifications');
}
return status;
} catch (error) {
console.error('Failed to verify notification delivery:', error);
throw error;
}
}
// For mobile: verify push notification configuration
async function verifyPushNotificationSetup() {
// Check for push registration
const pushToken = await getPushToken();
if (!pushToken) {
console.error('No push token available. Push notifications are not configured.');
return false;
}
// Verify token is registered with ATP
try {
const result = await client.verifyPushToken(pushToken);
if (result.registered) {
console.log('Push token is registered and valid');
console.log('Last notification received:', result.last_notification_at);
return true;
} else {
console.error('Push token is not registered with ATP');
// Re-register the token
await client.registerPushToken(pushToken);
console.log('Push token has been re-registered');
return true;
}
} catch (error) {
console.error('Failed to verify push token:', error);
return false;
}
}
// For web: verify WebSocket connection
function verifyWebSocketConnection() {
if (!client.isConnected()) {
console.error('WebSocket is not connected. Notifications cannot be received.');
// Attempt to reconnect
client.connect();
return false;
}
console.log('WebSocket is connected and ready to receive notifications');
return true;
}
Notification Display Issues
- Symptoms
- Causes
- Solutions
- Notifications render incorrectly
- Missing action buttons
- Broken layouts or UI issues
- Styling conflicts
- Incorrect response type handling
- Missing UI components for specific response types
// Create a notification tester to verify display
function testNotificationDisplay() {
// Create test notifications for all response types
const testNotifications = [
{
id: 'test_simple',
title: 'Simple Response Test',
description: 'Testing simple response buttons',
actions: [
{ id: 'approve', label: 'Approve', response_type: 'simple' },
{ id: 'reject', label: 'Reject', response_type: 'simple' }
]
},
{
id: 'test_text',
title: 'Text Response Test',
description: 'Testing text input response',
actions: [
{
id: 'comment',
label: 'Comment',
response_type: 'text',
response_options: {
placeholder: 'Enter your comment',
multiline: true,
max_length: 500
}
}
]
},
{
id: 'test_select',
title: 'Select Response Test',
description: 'Testing dropdown selection response',
actions: [
{
id: 'choose',
label: 'Choose Option',
response_type: 'select',
response_options: {
options: [
{ value: 'option1', label: 'Option 1' },
{ value: 'option2', label: 'Option 2' },
{ value: 'option3', label: 'Option 3' }
],
allow_multiple: false,
required: true
}
}
]
},
{
id: 'test_form',
title: 'Form Response Test',
description: 'Testing form input response',
actions: [
{
id: 'submit_form',
label: 'Submit Form',
response_type: 'form',
response_options: {
fields: [
{
id: 'name',
label: 'Name',
type: 'text',
required: true
},
{
id: 'age',
label: 'Age',
type: 'number',
min: 18,
max: 120
},
{
id: 'notes',
label: 'Notes',
type: 'textarea'
}
]
}
}
]
}
];
// Display each test notification
testNotifications.forEach(notification => {
displayNotification(notification);
});
}
// Test response handling without sending actual responses
function testResponseHandling(notification) {
// Mock response handler
function mockResponseHandler(actionId, responseData) {
console.log('Action handled:', actionId);
console.log('Response data:', responseData);
// Verify response data format based on response type
const action = notification.actions.find(a => a.id === actionId);
if (!action) {
console.error('Unknown action ID:', actionId);
return false;
}
switch (action.response_type) {
case 'simple':
console.log('Simple response validation: PASS');
return true;
case 'text':
if (typeof responseData !== 'string') {
console.error('Text response should be a string');
return false;
}
const { min_length, max_length } = action.response_options || {};
if (min_length && responseData.length < min_length) {
console.error(`Text too short (min: ${min_length})`);
return false;
}
if (max_length && responseData.length > max_length) {
console.error(`Text too long (max: ${max_length})`);
return false;
}
console.log('Text response validation: PASS');
return true;
case 'select':
const { options, allow_multiple } = action.response_options || {};
if (allow_multiple) {
if (!Array.isArray(responseData)) {
console.error('Multiple select response should be an array');
return false;
}
const allValid = responseData.every(value =>
options.some(opt => opt.value === value)
);
if (!allValid) {
console.error('Invalid option selected');
return false;
}
} else {
const isValid = options.some(opt => opt.value === responseData);
if (!isValid) {
console.error('Invalid option selected');
return false;
}
}
console.log('Select response validation: PASS');
return true;
// Handle other response types...
default:
console.warn(`No validation for response type: ${action.response_type}`);
return true;
}
}
// Return the mock handler for use in display tests
return mockResponseHandler;
}
Response Handling Issues
Response Submission Failures
- Symptoms
- Causes
- Solutions
- “Failed to submit response” errors
- Timeouts when submitting responses
- Responses not being registered by ATP server
- Network connectivity issues
- Expired notifications
- Invalid response format
- Rate limiting
// Implement robust response submission with retries
async function submitResponseWithRetry(notificationId, actionId, responseData, options = {}) {
const maxRetries = options.maxRetries || 3;
const backoffFactor = options.backoffFactor || 1.5;
const initialBackoff = options.initialBackoff || 1000;
let lastError = null;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
// Check if notification is still valid before attempting submission
const status = await client.getNotificationStatus(notificationId);
if (status.status === 'expired') {
console.error('Cannot respond to expired notification');
return { success: false, error: 'Notification expired' };
}
if (status.status === 'responded') {
console.warn('Notification already has a response');
return { success: true, alreadyResponded: true };
}
// Submit the response
const result = await client.respondToNotification(
notificationId,
actionId,
responseData
);
console.log('Response submitted successfully');
return { success: true, result };
} catch (error) {
console.error(`Response submission attempt ${attempt + 1} failed:`, error);
lastError = error;
// Check for permanent errors that shouldn't be retried
if (error.status === 404) {
console.error('Notification not found');
return { success: false, error: 'Notification not found' };
}
if (error.status === 400) {
console.error('Invalid response format');
return { success: false, error: 'Invalid response format' };
}
// Handle rate limiting
if (error.status === 429) {
const retryAfter = parseInt(error.headers?.['retry-after'] || '5', 10);
console.warn(`Rate limited. Retrying after ${retryAfter} seconds.`);
// Wait for the specified time before retrying
await new Promise(resolve => setTimeout(resolve, retryAfter * 1000));
continue;
}
// For other errors, use exponential backoff
const backoffTime = initialBackoff * Math.pow(backoffFactor, attempt);
console.log(`Retrying in ${backoffTime}ms...`);
await new Promise(resolve => setTimeout(resolve, backoffTime));
}
}
// If we get here, all retries failed
console.error(`Failed to submit response after ${maxRetries} attempts`);
return { success: false, error: lastError?.message || 'Max retries exceeded' };
}
// Queue responses for offline scenarios
class ResponseQueue {
constructor(atpClient) {
this.atpClient = atpClient;
this.queue = [];
this.storageKey = 'atp_response_queue';
// Load queue from storage
this.loadQueue();
// Process queue when online
if (typeof window !== 'undefined' && window.addEventListener) {
window.addEventListener('online', () => this.processQueue());
}
}
// Add response to queue
enqueue(notificationId, actionId, responseData) {
const item = {
id: Date.now().toString(),
notificationId,
actionId,
responseData,
timestamp: Date.now(),
attempts: 0
};
this.queue.push(item);
this.saveQueue();
// Try to process immediately if online
if (navigator.onLine) {
this.processQueue();
}
return item.id;
}
// Process all queued responses
async processQueue() {
if (!navigator.onLine || this.queue.length === 0) {
return;
}
console.log(`Processing ${this.queue.length} queued responses`);
const currentQueue = [...this.queue];
// Process each item
for (const item of currentQueue) {
try {
// Check if notification is still valid
const status = await this.atpClient.getNotificationStatus(item.notificationId);
if (status.status === 'expired') {
console.warn(`Notification ${item.notificationId} has expired. Removing from queue.`);
this.removeFromQueue(item.id);
continue;
}
if (status.status === 'responded') {
console.warn(`Notification ${item.notificationId} already has a response. Removing from queue.`);
this.removeFromQueue(item.id);
continue;
}
// Submit the response
await this.atpClient.respondToNotification(
item.notificationId,
item.actionId,
item.responseData
);
console.log(`Successfully processed queued response: ${item.id}`);
this.removeFromQueue(item.id);
} catch (error) {
console.error(`Failed to process queued response ${item.id}:`, error);
// Update attempt count
this.updateAttemptCount(item.id);
// Remove if too many attempts or too old
const updatedItem = this.queue.find(i => i.id === item.id);
if (!updatedItem) continue;
const ageInHours = (Date.now() - updatedItem.timestamp) / (1000 * 60 * 60);
if (updatedItem.attempts >= 5 || ageInHours > 24) {
console.warn(`Removing failed response from queue after ${updatedItem.attempts} attempts`);
this.removeFromQueue(item.id);
}
}
}
this.saveQueue();
}
// Remove item from queue
removeFromQueue(id) {
this.queue = this.queue.filter(item => item.id !== id);
this.saveQueue();
}
// Update attempt count
updateAttemptCount(id) {
this.queue = this.queue.map(item => {
if (item.id === id) {
return { ...item, attempts: item.attempts + 1 };
}
return item;
});
this.saveQueue();
}
// Save queue to storage
saveQueue() {
if (typeof localStorage !== 'undefined') {
localStorage.setItem(this.storageKey, JSON.stringify(this.queue));
}
}
// Load queue from storage
loadQueue() {
if (typeof localStorage !== 'undefined') {
try {
const storedQueue = localStorage.getItem(this.storageKey);
if (storedQueue) {
this.queue = JSON.parse(storedQueue);
console.log(`Loaded ${this.queue.length} responses from queue`);
}
} catch (error) {
console.error('Failed to load response queue:', error);
this.queue = [];
}
}
}
}
Webhook Processing Errors
- Symptoms
- Causes
- Solutions
- Webhook endpoint returning errors
- Duplicate webhook deliveries
- Missing webhook responses
- Webhook signature verification issues
- Server errors when processing webhooks
- Non-idempotent webhook handling
// Implement idempotent webhook processing
function processWebhookIdempotently(payload) {
const { notification_id, action_id, response_id, response_data } = payload;
// Create a unique key for this webhook delivery
const webhookKey = `webhook:${response_id}`;
// Check if we've already processed this webhook
if (hasProcessedWebhook(webhookKey)) {
console.log(`Webhook ${response_id} already processed. Ignoring duplicate.`);
return { success: true, duplicate: true };
}
try {
// Process the webhook
// ...your business logic here...
// Mark webhook as processed
markWebhookAsProcessed(webhookKey);
return { success: true };
} catch (error) {
console.error('Error processing webhook:', error);
// Don't mark as processed if there was an error
// This allows retry attempts
return { success: false, error: error.message };
}
}
// Use a cache or database to track processed webhooks
function hasProcessedWebhook(key) {
// Using Redis example
return redisClient.exists(key);
}
function markWebhookAsProcessed(key) {
// Set with a TTL of 24 hours
redisClient.set(key, '1', 'EX', 86400);
}
// Express middleware for handling webhook errors
function webhookErrorHandler(err, req, res, next) {
console.error('Webhook error:', err);
// Log request details for debugging
console.error('Webhook request details:');
console.error('Headers:', req.headers);
console.error('Body:', req.body);
// Return appropriate status based on error type
if (err.name === 'SignatureVerificationError') {
return res.status(401).json({ error: 'Invalid signature' });
}
if (err.name === 'WebhookValidationError') {
return res.status(400).json({ error: 'Invalid webhook payload' });
}
// For server errors, return 500 but acknowledge receipt
// This prevents the ATP server from retrying
res.status(500).json({
error: 'Webhook processing error',
acknowledged: true
});
// Log to error monitoring service
if (process.env.NODE_ENV === 'production') {
errorMonitoringService.captureException(err, {
tags: { type: 'webhook' },
extra: {
headers: req.headers,
body: req.body
}
});
}
}
Mobile-Specific Issues
Push Notification Issues
- Symptoms
- Causes
- Solutions
- Push notifications not arriving on device
- Push notifications arriving but not displaying
- “Missing notification data” errors
- Incorrect FCM/APNS configuration
- Missing notification permissions
- Expired push tokens
- Background restrictions
// iOS Push Notification Troubleshooting
// Check notification permissions
func checkNotificationPermissions() {
UNUserNotificationCenter.current().getNotificationSettings { settings in
switch settings.authorizationStatus {
case .authorized:
print("Notification permissions granted")
self.registerForPushNotifications()
case .denied:
print("ERROR: Notification permissions denied")
// Show alert to direct user to settings
DispatchQueue.main.async {
self.showNotificationPermissionAlert()
}
case .notDetermined:
print("Notification permissions not determined")
self.requestNotificationPermissions()
case .provisional:
print("Notification permissions granted provisionally")
self.registerForPushNotifications()
default:
print("Unknown permission status")
}
}
}
// Request permissions
func requestNotificationPermissions() {
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in
if let error = error {
print("ERROR: Failed to request notification permissions: \(error)")
return
}
if granted {
print("Notification permissions granted")
DispatchQueue.main.async {
self.registerForPushNotifications()
}
} else {
print("ERROR: Notification permissions denied")
}
}
}
// Register for push notifications
func registerForPushNotifications() {
DispatchQueue.main.async {
UIApplication.shared.registerForRemoteNotifications()
}
}
// Handle token registration
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
let tokenString = deviceToken.map { String(format: "%02.2hhx", $0) }.joined()
print("Push token: \(tokenString)")
// Register with ATP
ATPClient.shared.registerPushToken(token: tokenString, type: "apns") { success, error in
if success {
print("Push token registered with ATP successfully")
} else if let error = error {
print("ERROR: Failed to register push token: \(error)")
}
}
}
// Handle registration failure
func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
print("ERROR: Failed to register for remote notifications: \(error)")
// Check for common errors
if let nsError = error as NSError? {
if nsError.code == 3010 {
print("Push notifications are not available in the simulator")
} else {
print("Unknown error: \(nsError.code)")
}
}
}
// Show alert to direct user to settings
func showNotificationPermissionAlert() {
let alert = UIAlertController(
title: "Notifications Disabled",
message: "Enable notifications in Settings to receive important updates.",
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel))
alert.addAction(UIAlertAction(title: "Settings", style: .default) { _ in
if let url = URL(string: UIApplication.openSettingsURLString) {
UIApplication.shared.open(url)
}
})
self.present(alert, animated: true)
}
// Android Push Notification Troubleshooting
class PushNotificationManager(private val context: Context) {
private val TAG = "PushManager"
// Check if FCM is available and properly configured
fun checkFcmAvailability() {
FirebaseMessaging.getInstance().token
.addOnCompleteListener { task ->
if (task.isSuccessful) {
val token = task.result
Log.d(TAG, "FCM token: $token")
registerTokenWithAtp(token)
} else {
Log.e(TAG, "Failed to get FCM token", task.exception)
// Check for common errors
val exception = task.exception
when {
exception is FirebaseMessagingException && exception.errorCode == "MISSING_INSTANCEID_SERVICE" ->
Log.e(TAG, "Firebase not properly initialized")
exception is IOException ->
Log.e(TAG, "Network error, check connectivity")
else ->
Log.e(TAG, "Unknown error: ${exception?.message}")
}
}
}
}
// Register token with ATP
private fun registerTokenWithAtp(token: String) {
val atpClient = (context.applicationContext as MyApplication).atpClient
atpClient.registerPushToken(token, "fcm")
.addOnSuccessListener {
Log.d(TAG, "Token registered with ATP successfully")
}
.addOnFailureListener { e ->
Log.e(TAG, "Failed to register token with ATP", e)
}
}
// Check notification permissions (Android 13+)
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
fun checkNotificationPermissions() {
val hasPermission = ContextCompat.checkSelfPermission(
context,
Manifest.permission.POST_NOTIFICATIONS
) == PackageManager.PERMISSION_GRANTED
if (hasPermission) {
Log.d(TAG, "Notification permission granted")
} else {
Log.e(TAG, "Notification permission not granted")
requestNotificationPermission()
}
}
// Request notification permission (Android 13+)
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
private fun requestNotificationPermission() {
if (context is Activity) {
ActivityCompat.requestPermissions(
context,
arrayOf(Manifest.permission.POST_NOTIFICATIONS),
NOTIFICATION_PERMISSION_REQUEST_CODE
)
} else {
Log.e(TAG, "Context is not an Activity, cannot request permissions")
}
}
// Check if notifications are enabled in app settings
fun areNotificationsEnabled(): Boolean {
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// Check channel settings for Android 8+
val channel = notificationManager.getNotificationChannel(CHANNEL_ID)
channel?.importance != NotificationManager.IMPORTANCE_NONE
} else {
// Check app notification settings for older Android versions
NotificationManagerCompat.from(context).areNotificationsEnabled()
}
}
// Create or update notification channel (Android 8+)
@RequiresApi(Build.VERSION_CODES.O)
fun createNotificationChannel() {
val channel = NotificationChannel(
CHANNEL_ID,
"ATP Notifications",
NotificationManager.IMPORTANCE_HIGH
).apply {
description = "Notifications from AI services"
enableLights(true)
lightColor = Color.BLUE
enableVibration(true)
}
val notificationManager = context.getSystemService(NotificationManager::class.java)
notificationManager.createNotificationChannel(channel)
Log.d(TAG, "Notification channel created/updated")
}
// Display a test notification
fun sendTestNotification() {
val notificationManager = NotificationManagerCompat.from(context)
val builder = NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_notification)
.setContentTitle("Test Notification")
.setContentText("This is a test notification to verify push notification setup")
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setAutoCancel(true)
try {
notificationManager.notify(1, builder.build())
Log.d(TAG, "Test notification sent")
} catch (e: SecurityException) {
Log.e(TAG, "Failed to send test notification: ${e.message}")
}
}
companion object {
private const val CHANNEL_ID = "atp_notifications"
private const val NOTIFICATION_PERMISSION_REQUEST_CODE = 123
}
}
Background Processing Limitations
- Symptoms
- Causes
- Solutions
- Notifications not processed when app is in background
- Delayed notifications
- Missing webhook responses
- Mobile OS background restrictions
- Battery optimization settings
- Background task limitations
// Android WorkManager for reliable background processing
class NotificationProcessor : Worker(appContext, workerParams) {
override fun doWork(): Result {
val notificationId = inputData.getString("notification_id") ?: return Result.failure()
return try {
val atpClient = (applicationContext as MyApplication).atpClient
// Fetch notification details
val notification = atpClient.getNotificationSync(notificationId)
// Process the notification
processNotification(notification)
Result.success()
} catch (e: Exception) {
Log.e(TAG, "Failed to process notification", e)
// Retry with backoff for transient errors
if (isTransientError(e)) {
Result.retry()
} else {
Result.failure()
}
}
}
private fun isTransientError(e: Exception): Boolean {
return e is IOException || e is TimeoutException
}
private fun processNotification(notification: ATPNotification) {
// Create a system notification
val notificationManager = NotificationManagerCompat.from(applicationContext)
val builder = NotificationCompat.Builder(applicationContext, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_notification)
.setContentTitle(notification.title)
.setContentText(notification.description)
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setAutoCancel(true)
// Add action buttons if appropriate
addActionButtons(builder, notification)
// Create a PendingIntent to open the notification details
val intent = Intent(applicationContext, NotificationActivity::class.java).apply {
putExtra("notification_id", notification.id)
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
}
val pendingIntent = PendingIntent.getActivity(
applicationContext,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
builder.setContentIntent(pendingIntent)
// Show the notification
notificationManager.notify(notification.id.hashCode(), builder.build())
}
private fun addActionButtons(builder: NotificationCompat.Builder, notification: ATPNotification) {
// Only add buttons for simple actions
notification.actions
.filter { it.responseType == "simple" }
.take(3) // Android limits to 3 action buttons
.forEach { action ->
val actionIntent = Intent(applicationContext, NotificationActionReceiver::class.java).apply {
action = ACTION_RESPOND
putExtra("notification_id", notification.id)
putExtra("action_id", action.id)
}
val pendingIntent = PendingIntent.getBroadcast(
applicationContext,
action.id.hashCode(),
actionIntent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
builder.addAction(0, action.label, pendingIntent)
}
}
companion object {
private const val TAG = "NotificationProcessor"
private const val CHANNEL_ID = "atp_notifications"
private const val ACTION_RESPOND = "com.example.app.ACTION_RESPOND"
fun enqueue(context: Context, notificationId: String) {
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
val inputData = workDataOf("notification_id" to notificationId)
val request = OneTimeWorkRequestBuilder<NotificationProcessor>()
.setConstraints(constraints)
.setInputData(inputData)
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 10, TimeUnit.SECONDS)
.build()
WorkManager.getInstance(context)
.enqueueUniqueWork(
"notification_$notificationId",
ExistingWorkPolicy.REPLACE,
request
)
}
}
}
// Receiver for notification actions
class NotificationActionReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val notificationId = intent.getStringExtra("notification_id") ?: return
val actionId = intent.getStringExtra("action_id") ?: return
// Process response in background
ResponseProcessor.enqueue(context, notificationId, actionId, null)
// Cancel the notification
val notificationManager = NotificationManagerCompat.from(context)
notificationManager.cancel(notificationId.hashCode())
}
}
// Worker for processing responses
class ResponseProcessor : Worker(appContext, workerParams) {
override fun doWork(): Result {
val notificationId = inputData.getString("notification_id") ?: return Result.failure()
val actionId = inputData.getString("action_id") ?: return Result.failure()
val responseData = inputData.getString("response_data")
return try {
val atpClient = (applicationContext as MyApplication).atpClient
// Submit the response
atpClient.respondToNotificationSync(notificationId, actionId, responseData)
Result.success()
} catch (e: Exception) {
Log.e(TAG, "Failed to submit response", e)
// Retry with backoff for transient errors
if (isTransientError(e)) {
Result.retry()
} else {
Result.failure()
}
}
}
private fun isTransientError(e: Exception): Boolean {
return e is IOException || e is TimeoutException
}
companion object {
private const val TAG = "ResponseProcessor"
fun enqueue(context: Context, notificationId: String, actionId: String, responseData: String?) {
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
val inputDataBuilder = Data.Builder()
.putString("notification_id", notificationId)
.putString("action_id", actionId)
if (responseData != null) {
inputDataBuilder.putString("response_data", responseData)
}
val request = OneTimeWorkRequestBuilder<ResponseProcessor>()
.setConstraints(constraints)
.setInputData(inputDataBuilder.build())
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 10, TimeUnit.SECONDS)
.build()
WorkManager.getInstance(context)
.enqueueUniqueWork(
"response_${notificationId}_$actionId",
ExistingWorkPolicy.REPLACE,
request
)
}
}
}
// iOS Background Processing
class BackgroundTaskManager {
static let shared = BackgroundTaskManager()
private let atpClient = ATPClient.shared
private let queue = OperationQueue()
// Register background tasks
func registerBackgroundTasks() {
BGTaskScheduler.shared.register(
forTaskWithIdentifier: "com.example.app.notification-processing",
using: nil
) { task in
self.handleNotificationProcessingTask(task: task as! BGProcessingTask)
}
BGTaskScheduler.shared.register(
forTaskWithIdentifier: "com.example.app.response-submission",
using: nil
) { task in
self.handleResponseSubmissionTask(task: task as! BGProcessingTask)
}
}
// Schedule background task for notification processing
func scheduleNotificationProcessing() {
let request = BGProcessingTaskRequest(identifier: "com.example.app.notification-processing")
request.requiresNetworkConnectivity = true
request.requiresExternalPower = false
do {
try BGTaskScheduler.shared.submit(request)
print("Scheduled notification processing task")
} catch {
print("Failed to schedule notification processing: \(error)")
}
}
// Schedule background task for response submission
func scheduleResponseSubmission() {
let request = BGProcessingTaskRequest(identifier: "com.example.app.response-submission")
request.requiresNetworkConnectivity = true
request.requiresExternalPower = false
do {
try BGTaskScheduler.shared.submit(request)
print("Scheduled response submission task")
} catch {
print("Failed to schedule response submission: \(error)")
}
}
// Handle notification processing task
private func handleNotificationProcessingTask(task: BGProcessingTask) {
// Schedule a new task for next time
scheduleNotificationProcessing()
// Create a task expiration handler
task.expirationHandler = {
self.queue.cancelAllOperations()
}
// Create an operation to process pending notifications
let operation = BlockOperation {
// Process any pending notifications
let pendingNotifications = self.getPendingNotifications()
for notification in pendingNotifications {
self.processNotification(notification)
}
}
// Set completion handler
operation.completionBlock = {
task.setTaskCompleted(success: !operation.isCancelled)
}
// Add operation to queue
queue.addOperation(operation)
}
// Handle response submission task
private func handleResponseSubmissionTask(task: BGProcessingTask) {
// Schedule a new task for next time
scheduleResponseSubmission()
// Create a task expiration handler
task.expirationHandler = {
self.queue.cancelAllOperations()
}
// Create an operation to submit pending responses
let operation = BlockOperation {
// Process any pending responses
let pendingResponses = self.getPendingResponses()
for response in pendingResponses {
self.submitResponse(
notificationId: response.notificationId,
actionId: response.actionId,
responseData: response.responseData
)
}
}
// Set completion handler
operation.completionBlock = {
task.setTaskCompleted(success: !operation.isCancelled)
}
// Add operation to queue
queue.addOperation(operation)
}
// Process notification
private func processNotification(_ notification: PendingNotification) {
// Your notification processing logic
}
// Submit response
private func submitResponse(notificationId: String, actionId: String, responseData: Any?) {
// Your response submission logic
}
// Get pending notifications from storage
private func getPendingNotifications() -> [PendingNotification] {
// Your logic to retrieve pending notifications
return []
}
// Get pending responses from storage
private func getPendingResponses() -> [PendingResponse] {
// Your logic to retrieve pending responses
return []
}
}
// Pending notification model
struct PendingNotification {
let id: String
let receivedAt: Date
}
// Pending response model
struct PendingResponse {
let notificationId: String
let actionId: String
let responseData: Any?
let createdAt: Date
let attempts: Int
}
Verification: Knowledge Check
Before proceeding, let’s verify your understanding of ATP troubleshooting:Next Steps
Now that you know how to troubleshoot common ATP issues, explore these additional resources:Best Practices
Learn recommended patterns and practices for robust ATP implementations
API Reference
Detailed API documentation for ATP implementation