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 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 logout() async { disconnect(); } // --- Products --- Map get _headers => { 'Content-Type': 'application/json', 'Authorization': 'Basic ${base64Encode(utf8.encode('$_consumerKey:$_consumerSecret'))}', }; // Helper to build WC API URIs Uri _getUri(String path, [Map? 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> 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 body = jsonDecode(response.body); return body.map((json) => Product.fromJson(json)).toList(); } else { throw Exception('Failed to load products'); } } // Legacy/Alias Future> getProducts({int page = 1, String search = ''}) => fetchProducts(page: page, search: search); @override Future 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 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 createProduct(Map 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 updateProduct(int id, Map 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 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> uploadImage(List 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> 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 body = jsonDecode(response.body); return body.map((json) => Variation.fromJson(json)).toList(); } else { throw Exception('Failed to load variations'); } } Future createVariation(int productId, Map 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 updateVariation(int productId, int variationId, Map 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 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'); } } }