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, Next.js proxy, or Python proxy 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.
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.
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.