Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions app.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,19 @@
}
}
],
[
"@bacons/apple-targets",
{
"targets": [
{
"type": "widget",
"name": "widget",
"directory": "targets/widget",
"icon": "./src/static/icon.png"
}
]
}
],
["./plugins/withCustomGradleProperties"],
["./plugins/withWalletConnectScheme"],
["./plugins/withWalletQueries"],
Expand Down Expand Up @@ -63,6 +76,7 @@
"infoPlist": {
"ITSAppUsesNonExemptEncryption": false,
"NSCameraUsageDescription": "This app uses the camera to scan QR codes for addresses.",
"NSSupportsLiveActivities": true,
"LSApplicationQueriesSchemes": [
"metamask",
"trust",
Expand Down
260 changes: 260 additions & 0 deletions docs/live-activity-backend.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
# Backend Updates for Live Activities

## Overview
There are 3 ways to update Live Activities:

### 1. From the App (Local)
```typescript
// Update locally from React Native
await updateLiveActivity(activityId, 'New message from app');
```

### 2. From Backend (Remote Push)
Your backend sends push notifications to update the Live Activity remotely.

### 3. Background Fetch
App fetches data and updates locally.

---

## Backend Implementation (Option 2)

### Step 1: Get Push Token from App
```typescript
const result = await startLiveActivity('Initial message');
console.log('Push Token:', result.pushToken);

// Send this push token to your backend
await fetch('https://your-api.com/live-activity/register', {
method: 'POST',
body: JSON.stringify({
activityId: result.activityId,
pushToken: result.pushToken,
userId: currentUser.id
})
});
```

### Step 2: Backend Sends Push Notification

#### Node.js Example (using apn)
```javascript
const apn = require('apn');

// Configure APNs
const options = {
token: {
key: './AuthKey_XXXXXXXXXX.p8', // Your APNs auth key
keyId: 'XXXXXXXXXX', // Key ID
teamId: 'XXXXXXXXXX' // Team ID
},
production: false // Use true for production
};

const apnProvider = new apn.Provider(options);

// Send update to Live Activity
async function updateLiveActivity(pushToken, newMessage) {
const notification = new apn.Notification();

notification.topic = 'com.shapeShift.shapeShift.push-type.liveactivity';
notification.pushType = 'liveactivity';
notification.payload = {
aps: {
timestamp: Math.floor(Date.now() / 1000),
event: 'update',
'content-state': {
message: newMessage
}
}
};

const result = await apnProvider.send(notification, pushToken);
console.log('APNs result:', result);
}

// Example: Update when price changes
async function onPriceChange(userId, asset, newPrice) {
const pushToken = await db.getUserLiveActivityToken(userId);
if (pushToken) {
await updateLiveActivity(
pushToken,
`${asset} price: $${newPrice}`
);
}
}
```

#### Python Example (using aioapns)
```python
from aioapns import APNs, NotificationRequest

async def update_live_activity(push_token: str, new_message: str):
apns = APNs(
key_path='./AuthKey_XXXXXXXXXX.p8',
key_id='XXXXXXXXXX',
team_id='XXXXXXXXXX',
topic='com.shapeShift.shapeShift.push-type.liveactivity',
use_sandbox=True
)

request = NotificationRequest(
device_token=push_token,
message={
'aps': {
'timestamp': int(time.time()),
'event': 'update',
'content-state': {
'message': new_message
}
}
}
)

await apns.send_notification(request)

# Example: WebSocket updates
async def on_websocket_message(user_id, data):
push_token = await get_user_push_token(user_id)
if push_token:
await update_live_activity(
push_token,
f"New transaction: {data['amount']} {data['asset']}"
)
```

#### cURL Example (for testing)
```bash
# Get JWT token first
JWT_TOKEN=$(node -e "
const jwt = require('jsonwebtoken');
const fs = require('fs');
const key = fs.readFileSync('./AuthKey_XXXXXXXXXX.p8');
const token = jwt.sign({}, key, {
algorithm: 'ES256',
keyid: 'XXXXXXXXXX',
issuer: 'XXXXXXXXXX'
});
console.log(token);
")

# Send push notification
curl -v \
-H "authorization: bearer $JWT_TOKEN" \
-H "apns-push-type: liveactivity" \
-H "apns-topic: com.shapeShift.shapeShift.push-type.liveactivity" \
--http2 \
-d '{
"aps": {
"timestamp": '$(date +%s)',
"event": "update",
"content-state": {
"message": "Updated from backend via cURL!"
}
}
}' \
https://api.sandbox.push.apple.com:443/3/device/YOUR_PUSH_TOKEN_HERE
```

---

## Data Structure

### ContentState (what you can update)
```swift
// In iOS code (already defined)
struct ContentState: Codable, Hashable {
var message: String
// Add more fields as needed:
// var price: Double
// var asset: String
// var timestamp: Date
}
```

### Push Payload Format
```json
{
"aps": {
"timestamp": 1234567890,
"event": "update",
"content-state": {
"message": "Your custom message here"
}
}
}
```

---

## Real-World Use Cases

### 1. **Crypto Price Updates**
```typescript
// App
const result = await startLiveActivity('BTC: $45,000');

// Backend (on price change)
await updateLiveActivity(pushToken, 'BTC: $46,200 📈');
```

### 2. **Transaction Status**
```typescript
// App
const result = await startLiveActivity('Sending transaction...');

// Backend (on confirmation)
await updateLiveActivity(pushToken, 'Transaction confirmed! ✅');
```

### 3. **DEX Swap Progress**
```typescript
// App
const result = await startLiveActivity('Swapping ETH → USDC');

// Backend updates
await updateLiveActivity(pushToken, 'Waiting for approval...');
// ... later ...
await updateLiveActivity(pushToken, 'Swap complete! 🎉');
```

---

## Frequency Limits

- **Budget-based**: iOS gives you a limited budget for updates
- **Recommended**: Max 1-2 updates per minute
- **Best practice**: Only send updates when data actually changes

---

## Testing

1. **Local test** (from app):
```typescript
const result = await startLiveActivity('Test message');
await new Promise(resolve => setTimeout(resolve, 3000));
await updateLiveActivity(result.activityId, 'Updated locally!');
```

2. **Backend test**: Use the cURL example above with your push token

3. **Monitor logs**: Check Xcode console for `[Live Activity]` logs

---

## APNs Setup Required

1. Create an **APNs Auth Key** in Apple Developer Portal
2. Download the `.p8` file
3. Note your **Key ID** and **Team ID**
4. Add to your backend configuration

---

## Security Notes

- **Never expose** your `.p8` key file
- **Store push tokens securely** in your database
- **Validate** push token before sending
- **Rate limit** backend updates to prevent abuse
43 changes: 40 additions & 3 deletions eas.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,52 @@
"development": {
"autoIncrement": true,
"channel": "development",
"developmentClient": true
"developmentClient": true,
"distribution": "internal",
"ios": {
"appExtensions": [
{
"targetName": "widget",
"bundleIdentifier": "com.shapeShift.shapeShift.widget"
},
{
"targetName": "live-activity",
"bundleIdentifier": "com.shapeShift.shapeShift.live-activity"
}
]
}
},
"preview": {
"channel": "staging",
"distribution": "internal"
"distribution": "internal",
"ios": {
"appExtensions": [
{
"targetName": "widget",
"bundleIdentifier": "com.shapeShift.shapeShift.widget"
},
{
"targetName": "live-activity",
"bundleIdentifier": "com.shapeShift.shapeShift.live-activity"
}
]
}
},
"production": {
"autoIncrement": true,
"channel": "production"
"channel": "production",
"ios": {
"appExtensions": [
{
"targetName": "widget",
"bundleIdentifier": "com.shapeShift.shapeShift.widget"
},
{
"targetName": "live-activity",
"bundleIdentifier": "com.shapeShift.shapeShift.live-activity"
}
]
}
},
"dapp-store": {
"autoIncrement": true,
Expand Down
6 changes: 6 additions & 0 deletions modules/expo-live-activity/expo-module.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"platforms": ["apple"],
"apple": {
"modules": ["ExpoLiveActivityModule"]
}
}
5 changes: 5 additions & 0 deletions modules/expo-live-activity/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"name": "expo-live-activity",
"version": "1.0.0",
"main": "./src/index.ts"
}
39 changes: 39 additions & 0 deletions modules/expo-live-activity/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { requireNativeModule } from 'expo-modules-core'

export interface LiveActivityResult {
success: boolean
activityId?: string
pushToken?: string // Token for backend to send push updates
error?: string
}

/**
* Start a Live Activity that will appear on the lock screen and Dynamic Island
* @param message Message to display in the Live Activity
* @returns Promise with result containing activityId and pushToken if successful
*/
export async function startLiveActivity(message: string): Promise<LiveActivityResult> {
const ExpoLiveActivityModule = requireNativeModule('ExpoLiveActivity')
return await ExpoLiveActivityModule.startActivity(message)
}

/**
* Update a Live Activity with new content (local update from app)
* @param activityId The activity ID returned from startLiveActivity
* @param message New message to display
* @returns Promise with success status
*/
export async function updateLiveActivity(activityId: string, message: string): Promise<LiveActivityResult> {
const ExpoLiveActivityModule = requireNativeModule('ExpoLiveActivity')
return await ExpoLiveActivityModule.updateActivity(activityId, message)
}

/**
* End a Live Activity
* @param activityId The activity ID returned from startLiveActivity
* @returns Promise with success status
*/
export async function endLiveActivity(activityId: string): Promise<LiveActivityResult> {
const ExpoLiveActivityModule = requireNativeModule('ExpoLiveActivity')
return await ExpoLiveActivityModule.endActivity(activityId)
}
Loading