273 lines
8.3 KiB
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');
|
|
}
|
|
}
|
|
}
|
|
|