Skip to content

Commit 52acdc8

Browse files
feat: integrate event streaming in smart investing app
1 parent d16221d commit 52acdc8

File tree

9 files changed

+381
-38
lines changed

9 files changed

+381
-38
lines changed

smart_investing/android/app/src/main/AndroidManifest.xml

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22
<application
33
android:label="smart_investing"
44
android:name="${applicationName}"
5-
android:icon="@mipmap/ic_launcher"
6-
android:usesCleartextTraffic="true">
5+
android:usesCleartextTraffic="true"
6+
android:icon="@mipmap/ic_launcher">
77
<activity
88
android:name=".MainActivity"
99
android:exported="true"
10-
android:launchMode="singleTop"
10+
android:launchMode="singleTask"
1111
android:taskAffinity=""
1212
android:theme="@style/LaunchTheme"
1313
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
@@ -55,7 +55,8 @@
5555

5656
<activity
5757
android:name="com.smallcase.loans.features.ScLoanCustomTabActivity"
58-
android:exported="true">
58+
android:exported="true"
59+
android:excludeFromRecents="true">
5960
<intent-filter>
6061
<action android:name="android.intent.action.VIEW" />
6162

smart_investing/ios/Podfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ platform :ios, '14.0'
33

44
source 'https://cdn.cocoapods.org'
55
# private podspec for smallcase
6-
source 'https://github.com/smallcase/cocoapodspec-internal.git'
6+
# source 'https://github.com/smallcase/cocoapodspec-internal.git'
77

88
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
99
ENV['COCOAPODS_DISABLE_STATS'] = 'true'

smart_investing/lib/app/SILoansPage.dart

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@ import 'package:flutter/material.dart';
22
import 'package:go_router/go_router.dart';
33
import 'package:scloans/sc_loan.dart';
44

5-
import '../main.dart';
65
import 'global/SmartInvestingAppRepository.dart';
76
import 'widgets/SIButton.dart';
87
import 'widgets/SIEnvironmentController.dart';
98
import 'widgets/SISwitch.dart';
109
import 'widgets/SIText.dart';
1110
import 'widgets/SITextField.dart';
11+
import 'package:flutter/widgets.dart';
1212

1313
class SILoansPage extends StatelessWidget {
1414
const SILoansPage({super.key});
@@ -144,6 +144,10 @@ class SILoansPage extends StatelessWidget {
144144
}),
145145
],
146146
),
147+
const SizedBox(height: 16),
148+
SIText.large(text: "Loans Events (live)"),
149+
const SizedBox(height: 8),
150+
_LoansEventsList(),
147151
StreamBuilder(
148152
stream: repository.siConfig,
149153
builder: (context, snapshot) {
@@ -185,3 +189,37 @@ class SILoansPage extends StatelessWidget {
185189
);
186190
}
187191
}
192+
193+
class _LoansEventsList extends StatelessWidget {
194+
@override
195+
Widget build(BuildContext context) {
196+
return StreamBuilder<List<Map<String, dynamic>>>(
197+
stream: repository.loansEvents.stream,
198+
builder: (context, snapshot) {
199+
final items = snapshot.data ?? const [];
200+
if (items.isEmpty) {
201+
return Text('No events yet', style: Theme.of(context).textTheme.bodySmall);
202+
}
203+
return ListView.separated(
204+
physics: const NeverScrollableScrollPhysics(),
205+
shrinkWrap: true,
206+
itemCount: items.length.clamp(0, 50),
207+
separatorBuilder: (_, __) => const Divider(height: 1),
208+
itemBuilder: (context, index) {
209+
final event = items[items.length - 1 - index];
210+
final ts = event['timestamp'] ?? DateTime.now().millisecondsSinceEpoch;
211+
final type = event['type']?.toString() ?? 'unknown';
212+
final data = event['data'];
213+
final dt = DateTime.fromMillisecondsSinceEpoch(ts).toLocal();
214+
final timeStr = '${dt.hour.toString().padLeft(2,'0')}:${dt.minute.toString().padLeft(2,'0')}:${dt.second.toString().padLeft(2,'0')}';
215+
return Padding(
216+
padding: const EdgeInsets.symmetric(vertical: 6),
217+
child: Text('[$timeStr] $type — ${data is Map ? data : data?.toString() ?? ''}',
218+
style: Theme.of(context).textTheme.bodySmall),
219+
);
220+
},
221+
);
222+
},
223+
);
224+
}
225+
}

smart_investing/lib/app/features/ConnectScreen.dart

Lines changed: 130 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
import 'dart:async';
12
import 'package:clipboard/clipboard.dart';
23
import 'package:flutter/material.dart';
34
import 'package:scgateway_flutter_plugin/scgateway_flutter_plugin.dart';
5+
import 'package:scgateway_flutter_plugin/scgateway_events.dart';
46

57
import '../../smartinvesting.dart';
68
import '../global/SIConfigs.dart';
@@ -27,9 +29,30 @@ class ConnectScreen extends StatefulWidget {
2729
}
2830

2931
class _ConnectScreenState extends State<ConnectScreen> {
32+
StreamSubscription<String>? _scGatewayEventsSubscription;
33+
final List<String> _scGatewayEvents = [];
34+
35+
@override
36+
void initState() {
37+
super.initState();
38+
_setupScGatewayEvents();
39+
}
40+
41+
void _setupScGatewayEvents() {
42+
_scGatewayEventsSubscription = ScgatewayEvents.eventStream.listen((jsonString) {
43+
setState(() {
44+
// Display the JSON string directly
45+
_scGatewayEvents.insert(0, jsonString);
46+
if (_scGatewayEvents.length > 20) {
47+
_scGatewayEvents.removeLast();
48+
}
49+
});
50+
});
51+
}
52+
3053
@override
3154
void dispose() {
32-
// TODO: implement dispose
55+
_scGatewayEventsSubscription?.cancel();
3356
super.dispose();
3457
}
3558

@@ -96,8 +119,7 @@ class _ConnectScreenState extends State<ConnectScreen> {
96119
SIButton(
97120
label: "SETUP",
98121
onPressed: () async {
99-
final environmentResponse =
100-
await ScgatewayFlutterPlugin.setConfigEnvironment(
122+
await ScgatewayFlutterPlugin.setConfigEnvironment(
101123
repository.scGatewayConfig.value.environment,
102124
repository.scGatewayConfig.value.gatewayName,
103125
repository
@@ -189,12 +211,116 @@ class _ConnectScreenState extends State<ConnectScreen> {
189211
},
190212
),
191213
],
192-
)
214+
),
215+
SizedBox(height: 20),
216+
_buildScGatewayEventsSection(),
193217
],
194218
);
195219
},
196220
),
197221
],
198222
);
199223
}
224+
225+
Widget _buildScGatewayEventsSection() {
226+
return Card(
227+
margin: EdgeInsets.symmetric(vertical: 8),
228+
child: Padding(
229+
padding: EdgeInsets.all(12),
230+
child: Column(
231+
crossAxisAlignment: CrossAxisAlignment.start,
232+
children: [
233+
Row(
234+
mainAxisAlignment: MainAxisAlignment.spaceBetween,
235+
children: [
236+
Text(
237+
'ScGateway Events',
238+
style: TextStyle(
239+
fontSize: 18,
240+
fontWeight: FontWeight.bold,
241+
),
242+
),
243+
Row(
244+
children: [
245+
IconButton(
246+
icon: Icon(Icons.clear_all),
247+
onPressed: () {
248+
setState(() {
249+
_scGatewayEvents.clear();
250+
});
251+
},
252+
tooltip: 'Clear Events',
253+
),
254+
Text('${_scGatewayEvents.length}'),
255+
],
256+
),
257+
],
258+
),
259+
SizedBox(height: 8),
260+
Container(
261+
height: 200,
262+
decoration: BoxDecoration(
263+
border: Border.all(color: Colors.grey),
264+
borderRadius: BorderRadius.circular(4),
265+
),
266+
child: _scGatewayEvents.isEmpty
267+
? Center(
268+
child: Text(
269+
'No ScGateway events yet...',
270+
style: TextStyle(
271+
color: Colors.grey[600],
272+
fontStyle: FontStyle.italic,
273+
),
274+
),
275+
)
276+
: ListView.builder(
277+
itemCount: _scGatewayEvents.length,
278+
itemBuilder: (context, index) {
279+
final eventString = _scGatewayEvents[index];
280+
return Container(
281+
padding: EdgeInsets.all(8),
282+
margin: EdgeInsets.symmetric(vertical: 2),
283+
decoration: BoxDecoration(
284+
color: index % 2 == 0 ? Colors.grey[50] : Colors.white,
285+
),
286+
child: Row(
287+
crossAxisAlignment: CrossAxisAlignment.start,
288+
children: [
289+
Text(
290+
'${_scGatewayEvents.length - index}. ',
291+
style: TextStyle(
292+
fontWeight: FontWeight.bold,
293+
color: Colors.green,
294+
),
295+
),
296+
Expanded(
297+
child: SelectableText(
298+
eventString,
299+
style: TextStyle(
300+
fontFamily: 'monospace',
301+
fontSize: 12,
302+
),
303+
),
304+
),
305+
IconButton(
306+
icon: Icon(Icons.copy, size: 16),
307+
onPressed: () {
308+
FlutterClipboard.copy(eventString);
309+
ScaffoldMessenger.of(context).showSnackBar(
310+
SnackBar(content: Text('Event copied!')),
311+
);
312+
},
313+
tooltip: 'Copy Event',
314+
),
315+
],
316+
),
317+
);
318+
},
319+
),
320+
),
321+
],
322+
),
323+
),
324+
);
325+
}
200326
}

smart_investing/lib/app/global/SmartInvestingAppRepository.dart

Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,17 @@
1+
import 'dart:async';
12
import 'dart:convert';
23

34
import 'package:clipboard/clipboard.dart';
45
import 'package:flutter/material.dart';
56
import 'package:flutter/widgets.dart';
67
import 'package:rxdart/rxdart.dart';
78
import 'package:scgateway_flutter_plugin/scgateway_flutter_plugin.dart';
9+
import 'package:scgateway_flutter_plugin/scgateway_events.dart';
10+
import 'package:scloans/sc_loan.dart';
11+
import 'package:scloans/sc_loan_events.dart';
812

913
import '../../smartinvesting.dart';
10-
import 'SIConfigs.dart';
14+
import 'SIConfigs.dart' as si;
1115

1216
SmartInvestingAppRepository get repository {
1317
return SmartInvestingAppRepository.singleton();
@@ -20,6 +24,25 @@ class SmartInvestingAppRepository {
2024
scGatewayConfig.value = element.scGatewayConfig;
2125
scLoanConfig.value = element.scLoanConfig;
2226
});
27+
28+
// Subscribe to native event streams and pipe to BehaviorSubjects
29+
_gatewaySub = ScgatewayEvents.eventStream.listen((jsonString) {
30+
// Parse and append gateway events
31+
final event = ScgatewayEvents.parseEvent(jsonString);
32+
if (event != null) {
33+
_appendGatewayEvent(event);
34+
} else {
35+
_appendGatewayEvent({"type": "raw", "data": jsonString, "timestamp": DateTime.now().millisecondsSinceEpoch});
36+
}
37+
}, onError: (e) {
38+
_appendGatewayEvent({"type": "error", "data": e.toString(), "timestamp": DateTime.now().millisecondsSinceEpoch});
39+
});
40+
41+
// _loansSub = ScLoanEvents.eventStream.listen((event) {
42+
// _appendLoansEvent(event);
43+
// }, onError: (e) {
44+
// _appendLoansEvent({"type": "error", "data": e.toString(), "timestamp": DateTime.now().millisecondsSinceEpoch});
45+
// });
2346
}
2447

2548
static SmartInvestingAppRepository? _singleton;
@@ -30,16 +53,22 @@ class SmartInvestingAppRepository {
3053
return SmartInvesting.fromEnvironment(_singleton!.scGatewayConfig.value);
3154
}
3255

33-
final environment = BehaviorSubject.seeded(SIEnvironment.PRODUCTION);
56+
final environment = BehaviorSubject.seeded(si.SIEnvironment.PRODUCTION);
3457

35-
final siConfig = BehaviorSubject.seeded(SIConfig(loansUserId: "020896"));
36-
final scGatewayConfig = BehaviorSubject.seeded(ScGatewayConfig.prod());
37-
final scLoanConfig = BehaviorSubject.seeded(ScLoanConfig.prod());
58+
final siConfig = BehaviorSubject.seeded(si.SIConfig(loansUserId: "020896"));
59+
final scGatewayConfig = BehaviorSubject.seeded(si.ScGatewayConfig.prod());
60+
final scLoanConfig = BehaviorSubject.seeded(si.ScLoanConfig.prod());
3861

3962
final smartInvestingUserId = BehaviorSubject<String?>.seeded(null);
4063
final customAuthToken = BehaviorSubject<String?>.seeded(null);
4164
var appState = BehaviorSubject<String>.seeded("/");
4265

66+
// Live events log
67+
final gatewayEvents = BehaviorSubject<List<Map<String, dynamic>>>.seeded(const []);
68+
final loansEvents = BehaviorSubject<List<Map<String, dynamic>>>.seeded(const []);
69+
StreamSubscription? _gatewaySub;
70+
StreamSubscription? _loansSub;
71+
4372
//SMT Screen
4473
final headerColor = BehaviorSubject<String?>.seeded(null);
4574
final headerOpacity = BehaviorSubject<double?>.seeded(null);
@@ -174,10 +203,30 @@ Future<String> triggerTransaction(
174203
}
175204

176205
dispose() {
206+
_gatewaySub?.cancel();
207+
_loansSub?.cancel();
177208
environment.close();
178209
scGatewayConfig.close();
179210
scLoanConfig.close();
180211
transactionID.close();
212+
gatewayEvents.close();
213+
loansEvents.close();
181214
_singleton = null;
182215
}
183216
}
217+
218+
extension on SmartInvestingAppRepository {
219+
void _appendGatewayEvent(Map<String, dynamic> event) {
220+
final current = List<Map<String, dynamic>>.from(gatewayEvents.value);
221+
if (current.length >= 200) current.removeAt(0);
222+
current.add(event);
223+
gatewayEvents.add(current);
224+
}
225+
226+
void _appendLoansEvent(Map<String, dynamic> event) {
227+
final current = List<Map<String, dynamic>>.from(loansEvents.value);
228+
if (current.length >= 200) current.removeAt(0);
229+
current.add(event);
230+
loansEvents.add(current);
231+
}
232+
}

0 commit comments

Comments
 (0)