woocommerce_inventory/mobile_app/lib/api_service.dart

273 lines
8.3 KiB
Dart

import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart';
import 'services/platform_service.dart';
import 'models/product.dart';
class ApiService with ChangeNotifier implements PlatformService {
String? _baseUrl;
String? _consumerKey;
String? _consumerSecret;
bool _isLoading = false;
String? _error;
bool get isLoading => _isLoading;
String? get error => _error;
bool get isLoggedIn => _baseUrl != null && _consumerKey != null && _consumerSecret != null;
ApiService();
void connect(String url, String key, String secret) {
_baseUrl = url;
_consumerKey = key;
_consumerSecret = secret;
notifyListeners();
}
Future<void> verifyCredentials(String url, String key, String secret) async {
_isLoading = true;
_error = null;
notifyListeners();
try {
// Normalize URL
String baseUrl = url.trim();
if (!baseUrl.startsWith('http')) baseUrl = 'https://$baseUrl';
if (baseUrl.endsWith('/')) baseUrl = baseUrl.substring(0, baseUrl.length - 1);
// Construct WC API URL
final apiUri = Uri.parse('$baseUrl/wp-json/wc/v3/system_status');
final response = await http.get(
apiUri,
headers: {
'Content-Type': 'application/json',
'Authorization': 'Basic ${base64Encode(utf8.encode('$key:$secret'))}',
},
);
if (response.statusCode != 200) {
String msg = 'Connection failed: ${response.statusCode}';
try {
final body = jsonDecode(response.body);
msg = body['message'] ?? msg;
} catch (_) {}
throw Exception(msg);
}
} catch (e) {
_error = e.toString().contains('FormatException')
? 'Invalid Response. Is this a WooCommerce site?'
: e.toString().replaceAll('Exception: ', '');
rethrow;
} finally {
_isLoading = false;
notifyListeners();
}
}
void disconnect() {
_baseUrl = null;
_consumerKey = null;
_consumerSecret = null;
notifyListeners();
}
Future<void> logout() async {
disconnect();
}
// --- Products ---
Map<String, String> get _headers => {
'Content-Type': 'application/json',
'Authorization': 'Basic ${base64Encode(utf8.encode('$_consumerKey:$_consumerSecret'))}',
};
// Helper to build WC API URIs
Uri _getUri(String path, [Map<String, String>? queryParams]) {
// Ensuring path starts with /wp-json/wc/v3
// We assume _baseUrl is just the domain e.g. https://site.com
// We append the internal implementation path
return Uri.parse('$_baseUrl/wp-json/wc/v3$path').replace(queryParameters: queryParams);
}
// PlatformService Implementation
@override
Future<List<Product>> fetchProducts({int page = 1, String search = ''}) async {
if (!isLoggedIn) throw Exception('Not logged in');
final uri = _getUri('/products', {
'page': page.toString(),
'per_page': '20',
'search': search,
});
final response = await http.get(uri, headers: _headers);
if (response.statusCode == 200) {
final List<dynamic> body = jsonDecode(response.body);
return body.map((json) => Product.fromJson(json)).toList();
} else {
throw Exception('Failed to load products');
}
}
// Legacy/Alias
Future<List<Product>> getProducts({int page = 1, String search = ''}) => fetchProducts(page: page, search: search);
@override
Future<Product> fetchProduct(int id) async {
if (!isLoggedIn) throw Exception('Not logged in');
final uri = _getUri('/products/$id');
final response = await http.get(uri, headers: _headers);
if (response.statusCode == 200) {
return Product.fromJson(jsonDecode(response.body));
} else {
throw Exception('Failed to load product');
}
}
Future<Product> getProduct(dynamic id) async {
if (!isLoggedIn) throw Exception('Not logged in');
final uri = _getUri('/products/$id');
final response = await http.get(uri, headers: _headers);
if (response.statusCode == 200) {
return Product.fromJson(jsonDecode(response.body));
} else {
throw Exception('Failed to load product');
}
}
Future<void> createProduct(Map<String, dynamic> productData) async {
if (!isLoggedIn) throw Exception('Not logged in');
final uri = _getUri('/products');
final response = await http.post(
uri,
headers: _headers,
body: jsonEncode(productData),
);
if (response.statusCode != 200 && response.statusCode != 201) {
throw Exception('Failed to create product: ${response.body}');
}
}
@override
Future<Product> updateProduct(int id, Map<String, dynamic> productData) async {
if (!isLoggedIn) throw Exception('Not logged in');
final uri = _getUri('/products/$id');
final response = await http.put(
uri,
headers: _headers,
body: jsonEncode(productData),
);
if (response.statusCode == 200) {
return Product.fromJson(jsonDecode(response.body));
} else {
throw Exception('Failed to update product: ${response.body}');
}
}
@override
Future<dynamic> deleteProduct(int id) async {
if (!isLoggedIn) throw Exception('Not logged in');
final uri = _getUri('/products/$id', {'force': 'true'});
final response = await http.delete(uri, headers: _headers);
if (response.statusCode == 200) {
return jsonDecode(response.body);
} else {
throw Exception('Failed to delete product');
}
}
Future<Map<String, dynamic>> uploadImage(List<int> bytes, String filename) async {
if (!isLoggedIn) throw Exception('Not logged in');
// POST /wp-json/wp/v2/media
final uri = Uri.parse('$_baseUrl/wp-json/wp/v2/media');
final request = http.MultipartRequest('POST', uri);
// Add Auth Header manually because MultipartRequest doesn't use the map
request.headers['Authorization'] = 'Basic ${base64Encode(utf8.encode('$_consumerKey:$_consumerSecret'))}';
request.headers['Content-Disposition'] = 'attachment; filename=$filename';
request.files.add(http.MultipartFile.fromBytes(
'file',
bytes,
filename: filename
));
final response = await request.send();
final respStr = await response.stream.bytesToString();
if (response.statusCode == 201) {
return jsonDecode(respStr);
} else {
throw Exception('Failed to upload image: $respStr');
}
}
// --- Variations ---
Future<List<Variation>> fetchVariations(int productId) async {
if (!isLoggedIn) throw Exception('Not logged in');
final uri = _getUri('/products/$productId/variations');
final response = await http.get(uri, headers: _headers);
if (response.statusCode == 200) {
final List<dynamic> body = jsonDecode(response.body);
return body.map((json) => Variation.fromJson(json)).toList();
} else {
throw Exception('Failed to load variations');
}
}
Future<Variation> createVariation(int productId, Map<String, dynamic> data) async {
if (!isLoggedIn) throw Exception('Not logged in');
final uri = _getUri('/products/$productId/variations');
final response = await http.post(uri, headers: _headers, body: jsonEncode(data));
if (response.statusCode == 200 || response.statusCode == 201) {
return Variation.fromJson(jsonDecode(response.body));
} else {
throw Exception('Failed to create variation: ${response.body}');
}
}
Future<Variation> updateVariation(int productId, int variationId, Map<String, dynamic> data) async {
if (!isLoggedIn) throw Exception('Not logged in');
final uri = _getUri('/products/$productId/variations/$variationId');
final response = await http.put(uri, headers: _headers, body: jsonEncode(data));
if (response.statusCode == 200) {
return Variation.fromJson(jsonDecode(response.body));
} else {
throw Exception('Failed to update variation: ${response.body}');
}
}
Future<void> deleteVariation(int productId, int variationId) async {
if (!isLoggedIn) throw Exception('Not logged in');
final uri = _getUri('/products/$productId/variations/$variationId', {'force': 'true'});
final response = await http.delete(uri, headers: _headers);
if (response.statusCode != 200) {
throw Exception('Failed to delete variation');
}
}
}