# Android, iOS and Flutter via a backend

Mobile apps call your own backend proxy, never the MetaPhrase secret-key endpoint directly.

First deploy [the HTML/Node proxy](https://metaphrase.msalman.com/docs/recipe-html.md), [Next.js proxy](https://metaphrase.msalman.com/docs/recipe-nextjs.md), or [Python proxy](https://metaphrase.msalman.com/docs/recipe-python.md) at your own HTTPS origin. Its `POST /api/metaphrase` accepts `{"text":"..."}` and returns the full MetaPhrase response. Replace the example `https://your-app.example/api/metaphrase` with that actual URL. The backend alone stores `METAPHRASE_API_KEY`. Apply your app's normal login/session protection to this proxy.

## iOS (SwiftUI)

Create a SwiftUI iOS application in Xcode and replace its app source with this complete file. Set `proxyURL` to your backend URL. No MetaPhrase key is included in the app.

```swift
import SwiftUI

struct WritingResponse: Decodable {
    struct Result: Decodable { let text: String }
    struct Failure: Decodable { let message: String }
    let result: Result?
    let error: Failure?
}

@main
struct WriterApp: App {
    var body: some Scene { WindowGroup { WriterView() } }
}

struct WriterView: View {
    @State private var text = ""
    @State private var output = ""
    @State private var error = ""
    @State private var busy = false
    private let proxyURL = URL(string: "https://your-app.example/api/metaphrase")!
    var body: some View {
        NavigationStack {
            VStack(alignment: .leading, spacing: 16) {
                Text("Your text").font(.headline)
                TextEditor(text: $text).frame(minHeight: 150).border(.secondary)
                Button(busy ? "Writing…" : "Paraphrase") { Task { await paraphrase() } }
                    .disabled(busy || text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
                if !error.isEmpty { Text(error).foregroundStyle(.red).accessibilityLabel("Error: \(error)") }
                ScrollView { Text(output).frame(maxWidth: .infinity, alignment: .leading).textSelection(.enabled) }
            }.padding().navigationTitle("My paraphraser")
        }
    }
    @MainActor private func paraphrase() async {
        busy = true; error = ""
        defer { busy = false }
        do {
            var request = URLRequest(url: proxyURL)
            request.httpMethod = "POST"
            request.timeoutInterval = 250
            request.setValue("application/json", forHTTPHeaderField: "Content-Type")
            request.httpBody = try JSONEncoder().encode(["text": text])
            let (data, response) = try await URLSession.shared.data(for: request)
            let value = try JSONDecoder().decode(WritingResponse.self, from: data)
            guard let http = response as? HTTPURLResponse, (200..<300).contains(http.statusCode), let result = value.result else {
                error = value.error?.message ?? "Please try again."
                return
            }
            output = result.text
        } catch { self.error = error.localizedDescription }
    }
}
```

## Android / iOS (Flutter)

Run `flutter create my_writer`, then `cd my_writer` and `flutter pub add http`. Replace `lib/main.dart` with this complete app. Launch with `flutter run --dart-define=BACKEND_URL=https://your-app.example/api/metaphrase`.

```dart
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;

void main() => runApp(const MaterialApp(home: WriterPage()));
class WriterPage extends StatefulWidget {
  const WriterPage({super.key});
  @override State<WriterPage> createState() => _WriterPageState();
}
class _WriterPageState extends State<WriterPage> {
  final input = TextEditingController();
  String output = '', error = '';
  bool busy = false;
  static const backend = String.fromEnvironment('BACKEND_URL');
  @override void dispose() { input.dispose(); super.dispose(); }
  Future<void> paraphrase() async {
    setState(() { busy = true; error = ''; });
    try {
      if (backend.isEmpty) throw StateError('Set BACKEND_URL to your backend proxy');
      final response = await http.post(Uri.parse(backend), headers:{'Content-Type':'application/json'}, body:jsonEncode({'text':input.text})).timeout(const Duration(seconds:250));
      final value = jsonDecode(response.body) as Map<String,dynamic>;
      if (response.statusCode >= 400) throw Exception(value['error']?['message'] ?? 'Please retry.');
      if (mounted) setState(() { output = value['result']['text'] as String; });
    } catch (failure) { if (mounted) setState(() { error = failure.toString(); }); }
    finally { if (mounted) setState(() { busy = false; }); }
  }
  @override Widget build(BuildContext context) => Scaffold(
    appBar:AppBar(title:const Text('My paraphraser')),
    body:Padding(padding:const EdgeInsets.all(20), child:ListView(children:[
      TextField(controller:input,maxLines:7,decoration:const InputDecoration(labelText:'Your text',border:OutlineInputBorder())),
      const SizedBox(height:16),
      FilledButton(onPressed:busy ? null : paraphrase, child:Text(busy ? 'Writing…' : 'Paraphrase')),
      if(error.isNotEmpty) Semantics(liveRegion:true,child:Text(error,style:TextStyle(color:Theme.of(context).colorScheme.error))),
      const SizedBox(height:20),SelectableText(output),
    ])),
  );
}
```

Keep Android's INTERNET permission enabled in the release manifest (`<uses-permission android:name="android.permission.INTERNET"/>` inside `<manifest>`, outside `<application>`). Use HTTPS so iOS App Transport Security and Android cleartext restrictions need no exceptions. `BACKEND_URL` is public app configuration, not a secret key. Never replace it with a MetaPhrase API key.
