partner_library_flutter 1.0.12
partner_library_flutter: ^1.0.12 copied to clipboard
Partner UI Integration Library for Flutter
example/lib/main.dart
import 'package:dart_jsonwebtoken/dart_jsonwebtoken.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:partner_library_flutter/partner_library_flutter.dart';
import 'package:shared_preferences/shared_preferences.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await dotenv.load(fileName: "lib/.env");
await PartnerLibrary.init(
"https://sbmsmartbankinguat.esbeeyem.com",
whitelistedDomains: [
"razorpay.com",
"sbmkycuat.esbeeyem.com",
"m2pfintech.com"
],
deviceBindingEnabled: false,
);
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
// ⬇️ Wrap everything in a “hostile shell” so back tries to pop parent first.
builder: (context, child) => HostileShell(child: child ?? const SizedBox()),
home: MyHome(),
);
}
}
/// Simulates a partner app that tries to pop routes on back.
/// Your WebView page should still consume back (keyboard → webview → pop).
class HostileShell extends StatelessWidget {
final Widget child;
const HostileShell({super.key, required this.child});
@override
Widget build(BuildContext context) {
return PopScope(
canPop: false,
onPopInvokedWithResult: (didPop, result) async {
debugPrint('[HOST] back received at app-shell, attempting parent pop…');
Navigator.of(context).maybePop();
},
child: child,
);
}
}
class MyHome extends StatefulWidget {
@override
_MyHomeState createState() => _MyHomeState();
}
class _MyHomeState extends State<MyHome> {
final _formKey = GlobalKey<FormState>();
final _nameController = TextEditingController();
final _emailController = TextEditingController();
final _phoneController = TextEditingController();
@override
void initState() {
super.initState();
_restoreSavedFields();
}
Future<void> _restoreSavedFields() async {
final prefs = await SharedPreferences.getInstance();
_nameController.text = prefs.getString('name') ?? '';
_emailController.text = prefs.getString('email') ?? '';
_phoneController.text = prefs.getString('phone') ?? '';
}
@override
void dispose() {
_nameController.dispose();
_emailController.dispose();
_phoneController.dispose();
super.dispose();
}
void _goToFlowRunner() {
if (!_formKey.currentState!.validate()) return;
final module = dotenv.env['MODULE']!;
final baseUrl = dotenv.env['BASE_URL']!;
final name = _nameController.text.trim();
final email = _emailController.text.trim();
final phone = _phoneController.text.trim();
Navigator.of(context).push(MaterialPageRoute(
builder: (_) => FlowRunnerPage(
module: module,
baseUrl: baseUrl,
name: name,
email: email,
phone: phone,
photo: '', // pass if you have one
),
));
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Form(
key: _formKey,
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Row(children: [
const Spacer(),
SizedBox(
height: 48,
child: TextButton(
onPressed: _goToFlowRunner,
child: const Text('Dashboard', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 20)),
),
),
]),
const SizedBox(height: 16),
const Text('User Details', style: TextStyle(fontSize: 30, fontWeight: FontWeight.bold)),
const SizedBox(height: 16),
TextFormField(
controller: _nameController,
decoration: const InputDecoration(labelText: 'Name', border: OutlineInputBorder()),
validator: (v) => (v == null || v.isEmpty) ? 'Please enter your name' : null,
),
const SizedBox(height: 16),
TextFormField(
controller: _emailController,
decoration: const InputDecoration(labelText: 'Email', border: OutlineInputBorder()),
validator: (v) => (v == null || v.isEmpty) ? 'Please enter your email' : null,
),
const SizedBox(height: 16),
TextFormField(
controller: _phoneController,
decoration: const InputDecoration(labelText: 'Phone number', border: OutlineInputBorder()),
validator: (v) => (v == null || v.isEmpty) ? 'Please enter your phone number' : null,
),
]),
),
),
),
);
}
}
/// A single page that runs the whole “create token -> open PartnerLibrary” flow.
/// It shows a loader, handles errors, and keeps all async work off the form page.
class FlowRunnerPage extends StatefulWidget {
final String module;
final String baseUrl;
final String name;
final String email;
final String phone;
final String photo;
const FlowRunnerPage({
super.key,
required this.module,
required this.baseUrl,
required this.name,
required this.email,
required this.phone,
required this.photo,
});
@override
State<FlowRunnerPage> createState() => _FlowRunnerPageState();
}
class _FlowRunnerPageState extends State<FlowRunnerPage> {
String? _error;
@override
void initState() {
super.initState();
// kick off the flow after the first frame so `context` is fully available
WidgetsBinding.instance.addPostFrameCallback((_) => _runFlow());
}
Future<void> _runFlow() async {
try {
// Save fields for next time (optional)
final prefs = await SharedPreferences.getInstance();
await prefs.setString('name', widget.name);
await prefs.setString('email', widget.email);
await prefs.setString('phone', widget.phone);
// === previous createToken logic (dart_jsonwebtoken) ===
final String kid = dotenv.env['KID']!;
final String clientSecret = dotenv.env['CLIENT_SECRET']!;
final Map<String, Object> headers = {
'kid': kid,
'typ': 'JWT',
'alg': 'HS256',
};
final Map<String, String> attributes = {
'name': widget.name,
'photo': widget.photo,
};
final jwt = JWT(
{
'email': widget.email,
'phone': widget.phone,
'attributes': attributes,
'module': widget.module,
},
header: headers,
);
// 5 minutes TTL
final token = await jwt.sign(
SecretKey(clientSecret),
expiresIn: const Duration(minutes: 5),
);
if (!mounted) return;
await PartnerLibrary.instance.open(
context,
widget.module,
token,
(WebViewCallback action) {
switch (action.type) {
case WebViewCallbackType.redirect:
debugPrint("Redirected with status: ${action.status}");
break;
case WebViewCallbackType.logout:
debugPrint("User logged out");
break;
}
},
);
if (!mounted) return;
Navigator.of(context).maybePop();
} catch (e, st) {
debugPrint('FlowRunner error: $e\n$st');
if (!mounted) return;
setState(() => _error = e.toString());
}
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Scaffold(
appBar: AppBar(title: const Text('Launching Dashboard')),
body: Center(
child: _error == null
? Column(
mainAxisSize: MainAxisSize.min,
children: const [
CircularProgressIndicator(),
SizedBox(height: 12),
Text('Please wait… preparing session'),
],
)
: Padding(
padding: const EdgeInsets.all(16),
child: Column(mainAxisSize: MainAxisSize.min, children: [
Icon(Icons.error_outline, size: 48, color: theme.colorScheme.error),
const SizedBox(height: 12),
Text('Something went wrong', style: theme.textTheme.titleMedium),
const SizedBox(height: 8),
Text(_error!, textAlign: TextAlign.center),
const SizedBox(height: 16),
FilledButton(
onPressed: () {
setState(() => _error = null);
_runFlow();
},
child: const Text('Retry'),
),
]),
),
),
);
}
}