cloudcard_flutter 0.0.7-dev-2
cloudcard_flutter: ^0.0.7-dev-2 copied to clipboard
Flutter SDK for card digitization and provisioning with Sudo
example/lib/main.dart
import 'dart:convert';
import 'dart:developer';
import 'package:cloudcard_flutter/cloudcard_flutter.dart';
import 'package:http/http.dart' as http;
import 'package:flutter/material.dart';
import 'dart:async';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'CloudCard Flutter Example',
theme: ThemeData(primarySwatch: Colors.blue, useMaterial3: true),
home: const CloudCardHome(),
);
}
}
class CloudCardHome extends StatefulWidget {
const CloudCardHome({Key? key}) : super(key: key);
@override
State<CloudCardHome> createState() => _CloudCardHomeState();
}
class _CloudCardHomeState extends State<CloudCardHome> {
final CloudCardFlutter _cloudCard = CloudCardFlutter();
bool _isInitialized = false;
bool? _isDefaultPaymentApp = false;
bool? _isNfcEnabled = null;
List<CardData> _cards = [];
TokensUsageSummary? _tokenSummary;
// Text controllers for card registration
final TextEditingController _cardNumberController = TextEditingController();
final TextEditingController _expiryController = TextEditingController();
final TextEditingController _cvvController = TextEditingController();
final TextEditingController _nameController = TextEditingController();
final TextEditingController _cardIdController = TextEditingController();
final TextEditingController _tokenController = TextEditingController();
@override
void initState() {
super.initState();
_initializeCloudCard();
}
@override
void dispose() {
_cardNumberController.dispose();
_expiryController.dispose();
_cvvController.dispose();
_nameController.dispose();
_cardIdController.dispose();
_tokenController.dispose();
super.dispose();
}
// Initialize the CloudCard SDK
Future<void> _initializeCloudCard() async {
try {
// Initialize with sandbox mode for development
await _cloudCard.init(isSandBox: true,
onCardScanned: (CloudCardEvent scannedResult){
print("Card Scanned Launch **********");
print("Result ${scannedResult.eventType} : ${scannedResult.appIsOnForeground} : ${scannedResult.message} : ${scannedResult.isSuccess} : ${scannedResult.amount}");
//Show custom notification of payment with some parameters returned
//scannedResult.isSuccess
//scannedResult.eventType
//Navigate to screen
// Navigator.push(context, MaterialPageRoute(builder: (_)=>DummyPage()));
},
onScanComplete: (CloudCardEvent scannedResult){
print("Card Scanned **********");
print("Result ${scannedResult.eventType} : ${scannedResult.appIsOnForeground} : ${scannedResult.message} : ${scannedResult.isSuccess} : ${scannedResult.amount} : ${scannedResult.merchant} : ${scannedResult.timestamp} : ${scannedResult.transactionCount}");
// Scan complete - Show UI update
},
);
// Check if NFC is enabled
final nfcEnabled = await _cloudCard.isNfcEnabled();
// Check if app is the default payment app
final isDefault = await _cloudCard.isDefaultPaymentApp();
log("Is Default ${isDefault}");
setState(() {
_isInitialized = true;
_isNfcEnabled = nfcEnabled;
_isDefaultPaymentApp = isDefault;
});
// Load cards after initialization
await _loadCards();
// Check token summary
await _checkTokenSummary();
} catch (e) {
_showErrorSnackBar("Failed to initialize CloudCard: $e");
}
}
// Load all registered cards
Future<void> _loadCards() async {
try {
final result = await _cloudCard.getCards();
if (result.status == Status.SUCCESS && result.data != null) {
setState(() {
if (result.data is List<dynamic>) {
_cards = result.data;
// (result.data as List)
// .map((card) => card)
// .toList();
} else {
_cards = [];
}
});
_loadTransHistory();
} else {
_showErrorSnackBar("Failed to load cards: ${result.message}");
}
} catch (e,s) {
print(e);
print(s);
_showErrorSnackBar("Error loading cards: $e");
}
}
Future<void> _loadTransHistory() async {
final res = await _cloudCard.getSavedTransactions();
if(res.status == Status.SUCCESS) {
final txs = (res.data as List<SavedTransaction>);
for(int i = 0; i<txs.length; i++){
print("Index $i");
print(txs[i].amount);
print(txs[i].atc);
print(txs[i].timestamp);
// print(DateTime.fromMillisecondsSinceEpoch(int.parse(txs[i].timestamp)));
}
}
}
// Check token summary
Future<void> _checkTokenSummary() async {
try {
final result = await _cloudCard.tokenSummary();
if (result.status == Status.SUCCESS && result.data != null) {
print(result.data);
setState(() {
_tokenSummary = TokensUsageSummary.fromMap(result.data);
});
}
} catch (e) {
_showErrorSnackBar("Error checking token summary: $e");
}
}
// Register a new card
Future<void> _registerNewCard({
required String walletId,
required String paymentAppInstanceId,
required String accountId,
String? secret,
String? jwtToken,
}) async {
try {
final regData = RegistrationData(
walletId: walletId,
paymentAppInstanceId: paymentAppInstanceId,
accountId: accountId,
secret: secret ?? '',
jwtToken: jwtToken ?? '',
expiryDate: '',
cardHolderName: '',
cardNumber: '',
);
final result = await _cloudCard.registerCard(regData);
if (result.status == Status.SUCCESS) {
_showSuccessSnackBar("Card registered successfully!");
// Clear form fields
_cardNumberController.clear();
_expiryController.clear();
_cvvController.clear();
_nameController.clear();
// Refresh card list
await _loadCards();
Navigator.pop(context);
} else {
_showErrorSnackBar("Failed to register card: ${result.message}");
}
} catch (e) {
_showErrorSnackBar("Error registering card: $e");
}
}
// Generate EMV QR for a card
Future<void> _generateEmvQr(String cardId) async {
try {
final result = await _cloudCard.getEmvQr(cardId: cardId, amount: "000000000100");
if (result.status == Status.SUCCESS && result.data != null) {
// Show QR code in dialog
showDialog(
context: context,
builder:
(context) => AlertDialog(
title: const Text('EMV QR Code'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Image.memory(result.data),
const SizedBox(height: 16),
Text('Scan this QR code to complete payment'),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Close'),
),
],
),
);
} else {
_showErrorSnackBar("Failed to generate QR: ${result.message}");
}
} catch (e) {
_showErrorSnackBar("Error generating QR: $e");
}
}
// Delete a card
Future<void> _deleteCard(String cardId) async {
try {
final result = await _cloudCard.deleteCard(cardId);
if (result.status == Status.SUCCESS) {
_showSuccessSnackBar("Card deleted successfully!");
await _loadCards();
} else {
_showErrorSnackBar("Failed to delete card: ${result.message}");
}
} catch (e) {
_showErrorSnackBar("Error deleting card: $e");
}
}
// Freeze or unfreeze a card
Future<void> _freezeUnfreezeCard(
String cardId,
bool isCurrentlyFrozen,
) async {
try {
final result = await _cloudCard.freezeUnfreezeCard(
isFreeze: !isCurrentlyFrozen,
cardId: cardId,
);
if (result.status == Status.SUCCESS) {
_showSuccessSnackBar(
isCurrentlyFrozen ? "Card unfrozen!" : "Card frozen!",
);
await _loadCards();
} else {
_showErrorSnackBar("Operation failed: ${result.message}");
}
} catch (e) {
_showErrorSnackBar("Error updating card status: $e");
}
}
// Wipe wallet data
Future<void> _wipeWallet() async {
try {
// Show confirmation dialog
final confirm = await showDialog<bool>(
context: context,
builder:
(context) => AlertDialog(
title: const Text('Wipe Wallet'),
content: const Text(
'Are you sure you want to delete all cards and wallet data? This action cannot be undone.',
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('Cancel'),
),
TextButton(
onPressed: () => Navigator.pop(context, true),
child: const Text(
'Wipe Data',
style: TextStyle(color: Colors.red),
),
),
],
),
);
if (confirm == true) {
final result = await _cloudCard.wipeWallet();
if (result.status == Status.SUCCESS) {
_showSuccessSnackBar("Wallet data wiped successfully!");
await _loadCards();
await _checkTokenSummary();
} else {
_showErrorSnackBar("Failed to wipe wallet: ${result.message}");
}
}
} catch (e) {
_showErrorSnackBar("Error wiping wallet: $e");
}
}
// Launch default payment app settings
Future<void> _launchPaymentSettings() async {
try {
final launched = await _cloudCard.launchDefaultPaymentAppSettings();
if (!launched) {
_showErrorSnackBar("Could not open payment app settings");
}
} catch (e) {
_showErrorSnackBar("Error opening settings: $e");
}
}
void _showSuccessSnackBar(String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(message), backgroundColor: Colors.green),
);
}
void _showErrorSnackBar(String message) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(message), backgroundColor: Colors.red),
);
}
// Show dialog to add a new card
void _showAddCardDialog() {
showDialog(
context: context,
builder:
(context) => AlertDialog(
title: const Text('Add New Card'),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: _cardIdController,
decoration: const InputDecoration(labelText: 'Card Id'),
),
TextField(
controller: _tokenController,
decoration: const InputDecoration(
labelText: 'Access Token',
),
keyboardType: TextInputType.number,
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Cancel'),
),
TextButton(
onPressed: () async {
// Navigator.pop(context);
final digitalizedCard = await digitalizeCard();
if (digitalizedCard != null && digitalizedCard['data'] != null) {
_registerNewCard(
walletId: digitalizedCard['data']['institutionId'],
paymentAppInstanceId: 'uniqueId',
accountId: digitalizedCard['data']['cardId'],
jwtToken: digitalizedCard['data']['token'],
secret: digitalizedCard['data']['secret']??"",
);
}
},
child: const Text('Register'),
),
],
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('CloudCard Wallet'),
actions: [
IconButton(
icon: const Icon(Icons.settings),
onPressed: () {
showModalBottomSheet(
context: context,
builder: (context) => _buildSettingsSheet(),
);
},
),
],
),
body:
!_isInitialized
? const Center(child: CircularProgressIndicator())
: _buildBody(),
floatingActionButton: FloatingActionButton(
onPressed: _showAddCardDialog,
child: const Icon(Icons.add),
),
);
}
Widget _buildBody() {
return RefreshIndicator(
onRefresh: () async {
await _loadCards();
await _checkTokenSummary();
},
child: Column(
children: [
_buildStatusBar(),
const Divider(),
_buildTokenSummary(),
const Divider(),
Expanded(
child:
_cards.isEmpty
? Center(
child: Text(
'No cards registered yet.\nTap + to add a new card.',
textAlign: TextAlign.center,
style: TextStyle(color: Colors.grey[600]),
),
)
: ListView.builder(
itemCount: _cards.length,
itemBuilder:
(context, index) => _buildCardItem(_cards[index]),
),
),
],
),
);
}
Widget _buildStatusBar() {
return Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
if (_isNfcEnabled != null)
Icon(Icons.nfc, color: _isNfcEnabled! ? Colors.green : Colors.red),
const SizedBox(width: 8),
Text(
_isNfcEnabled == null?
'NFC not available' : _isNfcEnabled!? 'NFC Enabled' : 'NFC Disabled',
style: TextStyle(
color: _isNfcEnabled == true ? Colors.green : Colors.red,
fontWeight: FontWeight.bold,
),
),
const Spacer(),
if(_isDefaultPaymentApp != null)
Icon(
_isDefaultPaymentApp! ? Icons.check_circle : Icons.warning,
color: _isDefaultPaymentApp! ? Colors.green : Colors.orange,
),
const SizedBox(width: 8),
if(_isDefaultPaymentApp != null)
Text(
_isDefaultPaymentApp! ? 'Default Payment App' : 'Not Default App',
style: TextStyle(
color: _isDefaultPaymentApp! ? Colors.green : Colors.orange,
fontWeight: FontWeight.bold,
),
),
],
),
);
}
Widget _buildTokenSummary() {
if (_tokenSummary == null) {
return const SizedBox.shrink();
}
return Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Token Summary',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
),
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildTokenStat(
'Total Tokens',
_tokenSummary!.totalTokens.toString(),
),
_buildTokenStat(
'Available Tokens',
_tokenSummary!.tokensBalance.toString(),
),
_buildTokenStat(
'Used NFC Tokens',
_tokenSummary!.usedNfcTokens.toString(),
),
_buildTokenStat(
'Used QR Tokens',
_tokenSummary!.usedQrTokens.toString(),
),
],
),
],
),
);
}
Widget _buildTokenStat(String label, String value) {
return Column(
children: [
Text(
value,
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 20),
),
SizedBox(
width: MediaQuery.of(context).size.width * 0.18,
child: Text(
label,
textAlign: TextAlign.center,
maxLines: 2,
style: TextStyle(color: Colors.grey[600], fontSize: 12),
),
),
],
);
}
Widget _buildCardItem(CardData card) {
final lastFour = card.maskedPan;
return Card(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
elevation: 4,
child: Container(
height: 200,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors:
!card.isActive
? [Colors.blueGrey.shade800, Colors.blueGrey.shade500]
: [Colors.blue.shade800, Colors.blue.shade500],
),
borderRadius: BorderRadius.circular(12),
),
child: Stack(
children: [
// Card content
Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Digital Card',
style: TextStyle(
color: Colors.white.withOpacity(0.8),
fontSize: 14,
),
),
Icon(
Icons.credit_card,
color: Colors.white.withOpacity(0.8),
),
],
),
const Spacer(),
Row(
children: [
Text(
'$lastFour',
style: const TextStyle(
color: Colors.white,
fontSize: 22,
letterSpacing: 2,
),
),
const Spacer(),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'EXPIRES',
style: TextStyle(
color: Colors.white.withOpacity(0.6),
fontSize: 10,
),
),
const SizedBox(height: 4),
Text(
card.exp,
style: const TextStyle(
color: Colors.white,
fontSize: 16,
),
),
],
),
],
),
const SizedBox(height: 16),
Row(
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'CARD HOLDER',
style: TextStyle(
color: Colors.white.withOpacity(0.6),
fontSize: 10,
),
),
const SizedBox(height: 4),
Text(
card.cardHolder,
style: const TextStyle(
color: Colors.white,
fontSize: 16,
),
),
],
),
],
),
],
),
),
// Frozen overlay
if (!card.isActive)
Container(
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.15),
borderRadius: BorderRadius.circular(12),
),
child: const Center(
child: Text(
'FROZEN',
style: TextStyle(
color: Colors.white,
fontSize: 32,
fontWeight: FontWeight.bold,
letterSpacing: 8,
),
),
),
),
// Actions panel
Positioned(
bottom: 0,
right: 0,
child: Row(
children: [
IconButton(
icon: Icon(
!card.isActive ? Icons.ac_unit : Icons.ac_unit_outlined,
color: Colors.white,
),
onPressed:
() => _freezeUnfreezeCard(card.id, !card.isActive),
tooltip: !card.isActive ? 'Unfreeze Card' : 'Freeze Card',
),
IconButton(
icon: const Icon(Icons.qr_code, color: Colors.white),
onPressed: () => _generateEmvQr(card.id),
tooltip: 'Generate QR Code',
),
IconButton(
icon: const Icon(Icons.delete_outline, color: Colors.white),
onPressed: () => _deleteCard(card.id),
tooltip: 'Delete Card',
),
],
),
),
],
),
),
);
}
Widget _buildSettingsSheet() {
return SafeArea(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text(
'Settings',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
if(_isDefaultPaymentApp != null)
ListTile(
leading: const Icon(Icons.payment),
title: const Text('Set as Default Payment App'),
subtitle: Text(
_isDefaultPaymentApp!
? 'App is currently set as default'
: 'App is not set as default',
),
onTap: _launchPaymentSettings,
),
ListTile(
leading: const Icon(Icons.sync),
title: const Text('Manual Key Replenishment'),
onTap: () async {
try {
final result = await _cloudCard.manualKeyReplenishment();
if (result.status == Status.SUCCESS) {
_showSuccessSnackBar("Keys replenished successfully!");
} else {
_showErrorSnackBar(
"Failed to replenish keys: ${result.message}",
);
}
} catch (e) {
_showErrorSnackBar("Error: $e");
}
},
),
ListTile(
leading: const Icon(Icons.delete_forever, color: Colors.red),
title: const Text(
'Wipe Wallet Data',
style: TextStyle(color: Colors.red),
),
onTap: _wipeWallet,
),
],
),
),
);
}
Future<Map?> digitalizeCard() async {
try {
final digitalizeUrl = Uri.parse(
'https://digital-api-sandbox.cardcore.cloud/institution/jwt/${_cardIdController.text}'
// 'https://api.sandbox.sudo.africa/cards/digitalize/${_cardIdController.text}',
);
final digitalizeRes = await http.get(
digitalizeUrl,
headers: {
'Accept': 'text/plain',
'Content-Type': 'application/json',
'platform': 'android',
'ngrok-skip-browser-warning': 'allow-any',
'Authorization': _tokenController.text,
},
);
final digitalizeData = json.decode(digitalizeRes.body.toString());
log(digitalizeData.toString(), name: 'digitalizeData');
return digitalizeData;
} catch (e) {
log(e.toString());
rethrow;
}
}
}
class DummyPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
// TODO: implement build
throw UnimplementedError();
}
}