# Mobile App Integration Guide - Data Pins API

## Overview

This guide provides complete instructions for integrating your Data Pins mobile application with the Cotton City Plumbing website API. The integration enables technicians to submit geo-tagged job photos from the field, which automatically appear on the website's Recent Jobs page.

## Architecture

The Cotton City Plumbing website exposes a **tRPC API** over HTTP that your mobile app can call using standard REST principles. The API uses **Manus OAuth** for authentication, which supports Google, Microsoft, and Apple sign-in methods.

### API Base URL

**Production**: `https://cottoncityplumbing.com/api/trpc`

**Development/Testing**: Use your Manus-provided preview URL (e.g., `https://3000-xxxxx.manusvm.computer/api/trpc`)

### Authentication Flow

The API uses cookie-based authentication with Manus OAuth. Your mobile app must:

1. Direct users to the Manus OAuth login page
2. Receive an authentication callback with session cookies
3. Include these cookies in all subsequent API requests

## Step 1: User Authentication

### OAuth Login Flow

When a technician opens your mobile app for the first time, redirect them to the Manus OAuth portal:

```
https://oauth.manus.im/authorize?app_id=YOUR_APP_ID&redirect_uri=YOUR_REDIRECT_URI
```

**Parameters:**
- `app_id`: Your Manus application ID (found in the Management UI → Settings → General)
- `redirect_uri`: Your mobile app's deep link URL (e.g., `datapins://auth/callback`)

### Handling the Callback

After successful authentication, Manus redirects back to your app with session cookies. Your mobile app must:

1. Capture the redirect URL
2. Extract and store the session cookies (`manus_session`)
3. Include these cookies in all API requests

### Example (React Native with Expo)

```javascript
import * as WebBrowser from 'expo-web-browser';
import * as Linking from 'expo-linking';

const APP_ID = 'your-app-id-here';
const REDIRECT_URI = Linking.createURL('auth/callback');

async function loginWithManus() {
  const authUrl = `https://oauth.manus.im/authorize?app_id=${APP_ID}&redirect_uri=${encodeURIComponent(REDIRECT_URI)}`;
  
  const result = await WebBrowser.openAuthSessionAsync(authUrl, REDIRECT_URI);
  
  if (result.type === 'success') {
    // Session cookies are automatically stored by the WebBrowser
    console.log('Authentication successful');
    return true;
  }
  
  return false;
}
```

### Example (Flutter)

```dart
import 'package:flutter_web_auth/flutter_web_auth.dart';

Future<bool> loginWithManus() async {
  const appId = 'your-app-id-here';
  const redirectUri = 'datapins://auth/callback';
  
  final authUrl = 'https://oauth.manus.im/authorize?app_id=$appId&redirect_uri=$redirectUri';
  
  try {
    final result = await FlutterWebAuth.authenticate(
      url: authUrl,
      callbackUrlScheme: 'datapins',
    );
    
    // Session cookies are automatically stored
    print('Authentication successful');
    return true;
  } catch (e) {
    print('Authentication failed: $e');
    return false;
  }
}
```

## Step 2: Submitting Job Pins

### API Endpoint

**URL**: `POST /api/trpc/jobPins.create`

**Authentication**: Required (session cookies from OAuth)

**Content-Type**: `application/json`

### Request Format

tRPC uses a specific JSON-RPC format. Your request body must follow this structure:

```json
{
  "0": {
    "json": {
      "title": "Tankless Water Heater Installation",
      "description": "Installed a new Navien tankless water heater for a residential customer. The system provides endless hot water and improved energy efficiency.",
      "serviceType": "Water Heaters",
      "address": "123 Main St, Lubbock, TX 79401",
      "latitude": "33.5779",
      "longitude": "-101.8552",
      "photoBase64": "[base64-encoded-photo-data]",
      "technicianName": "Mike D.",
      "customerReview": "Mike was fantastic! He arrived on time and the new heater works great.",
      "customerName": "John Smith",
      "rating": 5
    }
  }
}
```

### Field Specifications

| Field | Type | Required | Description | Example |
|-------|------|----------|-------------|---------|
| `title` | string | ✅ Yes | Job title (max 255 chars) | "Water Heater Installation - Tech Terrace" |
| `description` | string | ✅ Yes | 2-3 sentences about the work performed | "Installed a new Navien tankless..." |
| `serviceType` | string | ✅ Yes | Service category (max 100 chars) | "Water Heaters", "Drain Cleaning", "Gas Lines" |
| `address` | string | ✅ Yes | Full street address (max 500 chars) | "123 Main St, Lubbock, TX 79401" |
| `latitude` | string | ✅ Yes | GPS latitude coordinate | "33.5779" |
| `longitude` | string | ✅ Yes | GPS longitude coordinate | "-101.8552" |
| `photoBase64` | string | ✅ Yes | Base64-encoded JPEG/PNG photo | "iVBORw0KGgoAAAANSUhEUg..." |
| `technicianName` | string | ❌ No | Name of technician who completed the job | "Mike D." |
| `customerReview` | string | ❌ No | Customer testimonial/feedback | "Great service!" |
| `customerName` | string | ❌ No | Customer's name for attribution | "John Smith" |
| `rating` | number | ❌ No | Star rating (1-5) | 5 |

### Response Format

**Success (200 OK):**

```json
{
  "result": {
    "data": {
      "json": {
        "success": true,
        "id": 42
      }
    }
  }
}
```

**Error (401 Unauthorized):**

```json
{
  "error": {
    "message": "UNAUTHORIZED",
    "code": -32004
  }
}
```

### Example Implementation (React Native)

```javascript
import * as ImagePicker from 'expo-image-picker';
import * as Location from 'expo-location';

async function submitJobPin(jobData) {
  const API_URL = 'https://cottoncityplumbing.com/api/trpc/jobPins.create';
  
  try {
    const response = await fetch(API_URL, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      credentials: 'include', // Important: includes session cookies
      body: JSON.stringify({
        "0": {
          "json": jobData
        }
      }),
    });
    
    const result = await response.json();
    
    if (result.result?.data?.json?.success) {
      console.log('Job pin created with ID:', result.result.data.json.id);
      return result.result.data.json.id;
    } else {
      throw new Error(result.error?.message || 'Failed to create job pin');
    }
  } catch (error) {
    console.error('Error submitting job pin:', error);
    throw error;
  }
}

// Complete workflow: capture photo, get location, submit
async function captureAndSubmitJob() {
  // 1. Request permissions
  const cameraPermission = await ImagePicker.requestCameraPermissionsAsync();
  const locationPermission = await Location.requestForegroundPermissionsAsync();
  
  if (!cameraPermission.granted || !locationPermission.granted) {
    alert('Camera and location permissions are required');
    return;
  }
  
  // 2. Take photo
  const photo = await ImagePicker.launchCameraAsync({
    mediaTypes: ImagePicker.MediaTypeOptions.Images,
    quality: 0.8,
    base64: true,
  });
  
  if (photo.canceled) return;
  
  // 3. Get GPS location
  const location = await Location.getCurrentPositionAsync({
    accuracy: Location.Accuracy.High,
  });
  
  // 4. Reverse geocode to get address
  const addresses = await Location.reverseGeocodeAsync({
    latitude: location.coords.latitude,
    longitude: location.coords.longitude,
  });
  
  const address = addresses[0];
  const fullAddress = `${address.street}, ${address.city}, ${address.region} ${address.postalCode}`;
  
  // 5. Submit to API
  const jobData = {
    title: 'Water Heater Installation', // You can make this user-editable
    description: 'Installed a new tankless water heater.', // User-editable
    serviceType: 'Water Heaters', // User selects from dropdown
    address: fullAddress,
    latitude: location.coords.latitude.toString(),
    longitude: location.coords.longitude.toString(),
    photoBase64: photo.assets[0].base64,
    technicianName: 'Mike D.', // Get from user profile
    customerReview: '', // Optional: user can add
    customerName: '', // Optional: user can add
    rating: 5, // Optional: user can select
  };
  
  const jobId = await submitJobPin(jobData);
  alert(`Job pin created successfully! ID: ${jobId}`);
}
```

### Example Implementation (Flutter)

```dart
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:image_picker/image_picker.dart';
import 'package:geolocator/geolocator.dart';
import 'package:geocoding/geocoding.dart';

Future<int> submitJobPin(Map<String, dynamic> jobData) async {
  const apiUrl = 'https://cottoncityplumbing.com/api/trpc/jobPins.create';
  
  final response = await http.post(
    Uri.parse(apiUrl),
    headers: {
      'Content-Type': 'application/json',
    },
    body: jsonEncode({
      "0": {
        "json": jobData
      }
    }),
  );
  
  if (response.statusCode == 200) {
    final result = jsonDecode(response.body);
    final jobId = result['result']['data']['json']['id'];
    print('Job pin created with ID: $jobId');
    return jobId;
  } else {
    throw Exception('Failed to create job pin: ${response.body}');
  }
}

Future<void> captureAndSubmitJob() async {
  // 1. Take photo
  final picker = ImagePicker();
  final photo = await picker.pickImage(
    source: ImageSource.camera,
    imageQuality: 80,
  );
  
  if (photo == null) return;
  
  // 2. Convert to base64
  final bytes = await photo.readAsBytes();
  final base64Photo = base64Encode(bytes);
  
  // 3. Get GPS location
  final position = await Geolocator.getCurrentPosition(
    desiredAccuracy: LocationAccuracy.high,
  );
  
  // 4. Reverse geocode
  final placemarks = await placemarkFromCoordinates(
    position.latitude,
    position.longitude,
  );
  
  final place = placemarks.first;
  final fullAddress = '${place.street}, ${place.locality}, ${place.administrativeArea} ${place.postalCode}';
  
  // 5. Submit to API
  final jobData = {
    'title': 'Water Heater Installation',
    'description': 'Installed a new tankless water heater.',
    'serviceType': 'Water Heaters',
    'address': fullAddress,
    'latitude': position.latitude.toString(),
    'longitude': position.longitude.toString(),
    'photoBase64': base64Photo,
    'technicianName': 'Mike D.',
    'customerReview': '',
    'customerName': '',
    'rating': 5,
  };
  
  final jobId = await submitJobPin(jobData);
  print('Success! Job ID: $jobId');
}
```

## Step 3: Photo Optimization

To ensure fast uploads and good website performance, optimize photos before encoding to base64:

### Recommended Settings

- **Format**: JPEG
- **Quality**: 80-85%
- **Max Width**: 1920px
- **Max Height**: 1080px
- **File Size Target**: < 500KB

### Example (React Native with expo-image-manipulator)

```javascript
import * as ImageManipulator from 'expo-image-manipulator';

async function optimizePhoto(photoUri) {
  const manipulatedImage = await ImageManipulator.manipulateAsync(
    photoUri,
    [{ resize: { width: 1920 } }], // Maintains aspect ratio
    { compress: 0.8, format: ImageManipulator.SaveFormat.JPEG, base64: true }
  );
  
  return manipulatedImage.base64;
}
```

## Step 4: Testing

### Test with Sample Data

Before connecting your mobile app, test the API using a tool like Postman or curl:

```bash
curl -X POST https://cottoncityplumbing.com/api/trpc/jobPins.create \
  -H "Content-Type: application/json" \
  -H "Cookie: manus_session=YOUR_SESSION_COOKIE" \
  -d '{
    "0": {
      "json": {
        "title": "Test Job",
        "description": "This is a test job submission.",
        "serviceType": "Water Heaters",
        "address": "123 Test St, Lubbock, TX 79401",
        "latitude": "33.5779",
        "longitude": "-101.8552",
        "photoBase64": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==",
        "technicianName": "Test Tech"
      }
    }
  }'
```

### Verify on Website

After submitting a test job pin:

1. Visit `https://cottoncityplumbing.com/recent-jobs`
2. Your test job should appear in the job feed
3. The map should show a marker at the GPS coordinates
4. Clicking the marker should display job details

## Error Handling

### Common Errors

| Error Code | Message | Cause | Solution |
|------------|---------|-------|----------|
| 401 | UNAUTHORIZED | Missing or invalid session cookies | Re-authenticate with OAuth |
| 400 | Invalid input | Missing required fields or invalid format | Check request payload matches schema |
| 413 | Payload too large | Photo file size exceeds limit | Compress photo before encoding |
| 500 | Internal server error | Server-side issue | Contact support at help.manus.im |

### Retry Logic

Implement exponential backoff for failed requests:

```javascript
async function submitWithRetry(jobData, maxRetries = 3) {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      return await submitJobPin(jobData);
    } catch (error) {
      if (attempt === maxRetries) throw error;
      
      const delay = Math.pow(2, attempt) * 1000; // 2s, 4s, 8s
      console.log(`Retry ${attempt}/${maxRetries} after ${delay}ms`);
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }
}
```

## Security Best Practices

### 1. Secure Cookie Storage

Store session cookies securely using your platform's secure storage:

- **React Native**: Use `expo-secure-store` or `react-native-keychain`
- **Flutter**: Use `flutter_secure_storage`

### 2. HTTPS Only

Always use HTTPS for API requests. Never send authentication cookies over HTTP.

### 3. Input Validation

Validate all user inputs before submitting:

```javascript
function validateJobData(data) {
  if (!data.title || data.title.length > 255) {
    throw new Error('Title is required and must be under 255 characters');
  }
  
  if (!data.description || data.description.length < 10) {
    throw new Error('Description must be at least 10 characters');
  }
  
  if (!data.latitude || !data.longitude) {
    throw new Error('GPS coordinates are required');
  }
  
  if (!data.photoBase64) {
    throw new Error('Photo is required');
  }
  
  return true;
}
```

## Production Checklist

Before launching your mobile app:

- [ ] Test OAuth login flow with Google, Microsoft, and Apple
- [ ] Verify photo upload with various image sizes and formats
- [ ] Test GPS accuracy in different locations
- [ ] Implement offline queue for submissions when network is unavailable
- [ ] Add user-friendly error messages for all failure scenarios
- [ ] Test on both iOS and Android devices
- [ ] Verify job pins appear correctly on the website
- [ ] Set up analytics to track submission success rates
- [ ] Create user documentation for technicians
- [ ] Train technicians on how to use the app

## Support

For technical assistance:

- **API Issues**: Contact Manus support at https://help.manus.im
- **Website Issues**: Check the Management UI → Dashboard for error logs
- **Mobile App Issues**: Review your app's error logs and network requests

---

**Last Updated**: December 8, 2025  
**API Version**: 1.0  
**Author**: Manus AI
