savers_app_sdk 1.0.1
savers_app_sdk: ^1.0.1 copied to clipboard
Savers Flutter SDK exposing native features (maps, dial pad, browser), device ID/location and session utilities, a Savers URL generator, and a WebView message bridge to trigger native actions from web content.
example/lib/main.dart
import 'dart:convert';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:savers_app_sdk/savers_app_sdk.dart';
void main() {
runApp(const SaversDemoApp());
}
/// Same generateUrl payload shape as clo-app CashbackScreen (EMAIL auth).
const UrlInput demoSaversUrlInput = UrlInput(
profile: Profile(
userId: '18898',
firstname: 'Ramesh',
lastname: 'Kumar',
email: 'ramesh+2@saversapp.com',
phone: '+14153036866',
city: 'Mountain View',
zipcode: '94043',
dob: '',
pv: '1',
ev: '1',
),
authType: AuthType.email,
);
class SaversDemoApp extends StatelessWidget {
const SaversDemoApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Savers SDK Demo',
navigatorKey: navigationKey,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: const Color(0xFF2563EB)),
scaffoldBackgroundColor: const Color(0xFFF8FAFC),
),
home: const HomeScreen(),
);
}
}
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
String deviceId = '-';
String locationResult = '-';
String apiKey = '-';
String encryptionKey = '-';
String pRefCode = '-';
String generatedUrl = '-';
@override
void initState() {
super.initState();
if (Platform.environment.containsKey('FLUTTER_TEST')) {
return;
}
_boot();
}
Future<void> _boot() async {
try {
await _initializeSdk();
} catch (_) {}
await _fetchInfo();
}
Future<void> _initializeSdk() async {
try {
await SaversAppSDK.initialized(
// Same sandbox credentials clo-app uses for testm.saversapp.com
apiKey: '3xqpYp6CPn899166YXbEB1YLJIhfdj08BbCdfQdg',
encryptionKey: 'MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=',
pRefCode: 'Vd7IR',
authMode: 'EMAIL',
environment: SaversSdkHostedEnvironment.sandbox,
);
await setSessionId('123456789');
await setLocationCoordinates(37.7749, -122.4194);
} catch (_) {}
}
Future<void> _fetchInfo() async {
try {
final id = await getDeviceId();
setState(() => deviceId = id);
} catch (e) {
setState(() => deviceId = e.toString());
}
try {
final loc = await getLocationCoordinates();
setState(() {
locationResult = loc != null
? '${loc['lat']}, ${loc['lng']}'
: 'Location unavailable';
});
try {
final url = await generateUrl(demoSaversUrlInput);
setState(() => generatedUrl = url);
} catch (e) {
setState(() => generatedUrl = e.toString());
}
} catch (e) {
setState(() => locationResult = e.toString());
}
try {
final k = await getApiKey();
setState(() => apiKey = k);
} catch (e) {
setState(() => apiKey = e.toString());
}
try {
final ek = await getEncryptionKey();
setState(() => encryptionKey = ek);
} catch (e) {
setState(() => encryptionKey = e.toString());
}
try {
final pr = await getPRefCode();
setState(() => pRefCode = pr);
} catch (e) {
setState(() => pRefCode = e.toString());
}
}
Future<void> _handleAction(
String action,
Map<String, dynamic> payload,
) async {
handleWebMessage(
jsonEncode({'action': action, 'payload': payload}),
postBack: (_) {},
);
}
bool get _hasGeneratedUrl => Uri.tryParse(generatedUrl)?.hasScheme ?? false;
void _openHostedApp(String url) {
Navigator.of(context).push(
MaterialPageRoute(builder: (_) => HostedAppScreen(saversAppUrl: url)),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Column(
children: [
const _Header(),
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
child: Column(
children: [
_Card(
title: 'Navigation Test',
child: _PrimaryButton(
label: 'Open Close Screen',
color: const Color(0xFF3B82F6),
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => const CloseTestScreen(),
),
);
},
),
),
_InfoCard(title: 'Device ID', value: deviceId),
_InfoCard(title: 'Location', value: locationResult),
_InfoCard(title: 'API Key', value: apiKey, mono: true),
_InfoCard(
title: 'Encryption Key',
value: encryptionKey,
mono: true,
),
_InfoCard(
title: 'Program Ref Code',
value: pRefCode,
mono: true,
),
_Card(
title: 'Generated URL',
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_ValueBox(text: generatedUrl, mono: true),
if (_hasGeneratedUrl)
Padding(
padding: const EdgeInsets.only(top: 12),
child: Row(
children: [
Expanded(
child: _PrimaryButton(
label: 'Open in Browser',
color: const Color(0xFF3B82F6),
onPressed: () =>
openBrowser(generatedUrl),
),
),
const SizedBox(width: 8),
Expanded(
child: _PrimaryButton(
label: 'Open in WebView',
color: const Color(0xFF14B8A6),
onPressed: () =>
_openHostedApp(generatedUrl),
),
),
],
),
),
],
),
),
],
),
),
),
_ActionBar(onAction: _handleAction),
],
),
),
);
}
}
class HostedAppScreen extends StatelessWidget {
const HostedAppScreen({super.key, required this.saversAppUrl});
final String saversAppUrl;
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
body: HostedAppComponent(
saversAppUrl: saversAppUrl,
travelHeaderTopInset: TravelPortalHeaderInsets.safeArea,
onSaversSdkMessage: (raw, postBack) {
try {
final msg = jsonDecode(raw);
if (msg is Map && msg['action'] == 'END_SESSION') {
Navigator.of(context).maybePop();
return;
}
} catch (_) {}
handleWebMessage(raw, postBack: postBack);
},
),
);
}
}
class CloseTestScreen extends StatelessWidget {
const CloseTestScreen({super.key});
Future<void> _handleAction(String action) async {
handleWebMessage(jsonEncode({'action': action}));
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Column(
children: [
const _Card(
title: 'SDK Close Current Screen',
child: _ValueBox(
text:
'Press the button to close this screen via the SDK navigationKey.',
),
),
_MiniActionBar(onClose: () => _handleAction('END_SESSION')),
Padding(
padding: const EdgeInsets.only(top: 12),
child: _PrimaryButton(
label: 'Push Another Screen',
color: const Color(0xFF14B8A6),
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => const CloseTestScreen(),
),
);
},
),
),
],
),
),
),
);
}
}
class _Header extends StatelessWidget {
const _Header();
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
color: const Color(0xFF2563EB),
child: const Row(
children: [
Text(
'Savers SDK Demo',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w700,
color: Colors.white,
),
),
SizedBox(width: 12),
Text(
'Native features showcase',
style: TextStyle(fontSize: 12, color: Color(0xFFE5E7EB)),
),
],
),
);
}
}
class _Card extends StatelessWidget {
final String title;
final Widget child;
const _Card({required this.title, required this.child});
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
margin: const EdgeInsets.symmetric(vertical: 8),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: const [
BoxShadow(
color: Color(0x1A000000),
blurRadius: 8,
offset: Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(fontSize: 12, color: Color(0xFF6B7280)),
),
const SizedBox(height: 8),
child,
],
),
);
}
}
class _InfoCard extends StatelessWidget {
final String title;
final String value;
final bool mono;
const _InfoCard({
required this.title,
required this.value,
this.mono = false,
});
@override
Widget build(BuildContext context) {
return _Card(
title: title,
child: _ValueBox(text: value, mono: mono),
);
}
}
class _ValueBox extends StatelessWidget {
final String text;
final bool mono;
const _ValueBox({required this.text, this.mono = false});
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: const Color(0xFFF3F4F6),
borderRadius: BorderRadius.circular(8),
),
child: Text(
text,
style: TextStyle(
fontSize: mono ? 14 : 16,
color: const Color(0xFF111827),
),
),
);
}
}
class _PrimaryButton extends StatelessWidget {
final String label;
final Color color;
final VoidCallback onPressed;
const _PrimaryButton({
required this.label,
required this.color,
required this.onPressed,
});
@override
Widget build(BuildContext context) {
return SizedBox(
height: 40,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: color,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
onPressed: onPressed,
child: Text(
label,
style: const TextStyle(
color: Colors.white,
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
),
);
}
}
class _ActionBar extends StatelessWidget {
final Future<void> Function(String action, Map<String, dynamic> payload)
onAction;
const _ActionBar({required this.onAction});
@override
Widget build(BuildContext context) {
return Container(
height: 64,
margin: const EdgeInsets.fromLTRB(16, 0, 16, 12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFFE5E7EB)),
),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: SizedBox(
height: _actionButtonHeight,
child: ListView(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 16),
children: [
_ActionButton(
label: 'Open Map',
onPressed: () => onAction('OPEN_MAP', {
'lat': 37.7749,
'lng': -122.4194,
'label': 'SaverAppsPoint',
}),
),
_ActionButton(
label: 'Open Dialpad',
onPressed: () =>
onAction('SHOW_DIAL_PAD', {'number': '+1234567890'}),
),
_ActionButton(
label: 'Open Browser',
onPressed: () => onAction('MERCHANT_PORTAL_REDIRECT', {
'url': 'https://www.google.com',
}),
),
_ActionButton(
label: 'Set Session ID',
onPressed: () =>
onAction('SESSION_ID', {'sessionId': '123456789'}),
),
],
),
),
),
);
}
}
class _MiniActionBar extends StatelessWidget {
final VoidCallback onClose;
const _MiniActionBar({required this.onClose});
@override
Widget build(BuildContext context) {
return Container(
height: 64,
margin: const EdgeInsets.only(top: 12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFFE5E7EB)),
),
child: Center(
child: _ActionButton(label: 'Close Screen', onPressed: onClose),
),
);
}
}
const double _actionButtonHeight = 44;
class _ActionButton extends StatelessWidget {
final String label;
final VoidCallback? onPressed;
const _ActionButton({required this.label, required this.onPressed});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(right: 8),
child: OutlinedButton(
style: OutlinedButton.styleFrom(
backgroundColor: const Color(0xFFF5F5F5),
foregroundColor: const Color(0xFF2563EB),
disabledForegroundColor: const Color(0xFF9CA3AF),
disabledBackgroundColor: const Color(0xFFF5F5F5),
side: const BorderSide(color: Color(0xFFCCCCCC)),
padding: const EdgeInsets.symmetric(horizontal: 16),
minimumSize: const Size(0, _actionButtonHeight),
maximumSize: const Size(double.infinity, _actionButtonHeight),
fixedSize: const Size.fromHeight(_actionButtonHeight),
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
visualDensity: VisualDensity.compact,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
onPressed: onPressed,
child: Text(label, style: const TextStyle(fontSize: 16)),
),
);
}
}