From 8df8258bf31d6c83be3bb2da71bb538248b08eeb Mon Sep 17 00:00:00 2001 From: falcon_7 <117983962+SumitUni7@users.noreply.github.com> Date: Tue, 31 Mar 2026 11:59:05 +0530 Subject: [PATCH] Make project runnable with Flutter preview entrypoint --- README.md | 26 +++++ lib/features/bonds/README.md | 53 +++++++++++ .../mock_nse_cbrics_transport.dart | 69 ++++++++++++++ .../datasources/nse_cbrics_datasource.dart | 76 +++++++++++++++ .../bonds/data/models/bond_model.dart | 25 +++++ .../bonds/data/models/bond_order_model.dart | 70 ++++++++++++++ .../repositories/bond_repository_impl.dart | 33 +++++++ lib/features/bonds/domain/entities/bond.dart | 19 ++++ .../bonds/domain/entities/bond_order.dart | 29 ++++++ .../domain/repositories/bond_repository.dart | 16 ++++ .../domain/usecases/place_bond_order.dart | 36 +++++++ .../controllers/bond_controller.dart | 31 ++++++ .../bonds/presentation/pages/bond_page.dart | 95 +++++++++++++++++++ .../presentation/pages/bond_preview_page.dart | 26 +++++ .../widgets/bond_order_ticket.dart | 92 ++++++++++++++++++ lib/main.dart | 24 +++++ pubspec.yaml | 18 ++++ 17 files changed, 738 insertions(+) create mode 100644 README.md create mode 100644 lib/features/bonds/README.md create mode 100644 lib/features/bonds/data/datasources/mock_nse_cbrics_transport.dart create mode 100644 lib/features/bonds/data/datasources/nse_cbrics_datasource.dart create mode 100644 lib/features/bonds/data/models/bond_model.dart create mode 100644 lib/features/bonds/data/models/bond_order_model.dart create mode 100644 lib/features/bonds/data/repositories/bond_repository_impl.dart create mode 100644 lib/features/bonds/domain/entities/bond.dart create mode 100644 lib/features/bonds/domain/entities/bond_order.dart create mode 100644 lib/features/bonds/domain/repositories/bond_repository.dart create mode 100644 lib/features/bonds/domain/usecases/place_bond_order.dart create mode 100644 lib/features/bonds/presentation/controllers/bond_controller.dart create mode 100644 lib/features/bonds/presentation/pages/bond_page.dart create mode 100644 lib/features/bonds/presentation/pages/bond_preview_page.dart create mode 100644 lib/features/bonds/presentation/widgets/bond_order_ticket.dart create mode 100644 lib/main.dart create mode 100644 pubspec.yaml diff --git a/README.md b/README.md new file mode 100644 index 0000000..b1e8fd4 --- /dev/null +++ b/README.md @@ -0,0 +1,26 @@ +# Indian Bond Module Demo (Flutter) + +This repository now includes a runnable Flutter entrypoint that launches the NSE CBRICS Bonds preview UI using local mock data. + +## Run locally + +1. Ensure Flutter SDK is installed (`flutter --version`). +2. In this repository root, generate platform folders (only first time): + +```bash +flutter create . +``` + +3. Get dependencies: + +```bash +flutter pub get +``` + +4. Run app: + +```bash +flutter run +``` + +The app boots to `BondPreviewPage`, so it works without backend APIs. diff --git a/lib/features/bonds/README.md b/lib/features/bonds/README.md new file mode 100644 index 0000000..878211d --- /dev/null +++ b/lib/features/bonds/README.md @@ -0,0 +1,53 @@ +# Bond Module (Indian Market via NSE CBRICS) + +This module provides a practical baseline implementation for trading Indian bonds inside a Flutter app. + +## What is included + +- Domain entities for `Bond` and `BondOrder` +- Repository contract and implementation +- `PlaceBondOrder` use case for validation before OMS submission +- Data source for NSE CBRICS OMS-style REST endpoints +- Mock transport for local feature preview without backend +- Basic controller for loading instruments/orders and placing orders +- Starter UI: + - `BondPage` instrument list with pull-to-refresh + - `BondOrderTicket` order entry widget embedded per bond row + - `BondPreviewPage` for quick local demo + +## Expected NSE CBRICS endpoints + +- `GET /bonds` -> `{ "data": [ ...bond ] }` +- `POST /orders` -> returns either `{ "data": { ...order } }` or `{ ...order }` +- `GET /orders/open` -> `{ "data": [ ...order ] }` + +## Example wiring (real backend) + +```dart +final dataSource = NseCbricsDataSource( + baseUrl: 'https://your-nse-cbrics-host/api/v1', + apiKey: 'YOUR_API_KEY', + http: yourHttpTransport, +); + +final repository = BondRepositoryImpl(dataSource); +final controller = BondController(repository); + +MaterialApp(home: BondPage(controller: controller)); +``` + +## Preview wiring (no backend) + +```dart +MaterialApp(home: BondPreviewPage()); +``` + +`BondPreviewPage` uses `MockNseCbricsTransport` with in-memory sample bonds and orders so you can verify UI and order flow quickly. + +## Next steps for production + +- Add auth token refresh and request signing required by your broker +- Add order modification/cancel flows +- Add lot-size, tick-size, and risk-rule validation from live exchange masters +- Add pagination, bond search, and filter chips for yields/maturity buckets +- Add tests (unit + widget + API contract) diff --git a/lib/features/bonds/data/datasources/mock_nse_cbrics_transport.dart b/lib/features/bonds/data/datasources/mock_nse_cbrics_transport.dart new file mode 100644 index 0000000..39d7c4c --- /dev/null +++ b/lib/features/bonds/data/datasources/mock_nse_cbrics_transport.dart @@ -0,0 +1,69 @@ +import 'dart:convert'; + +import 'nse_cbrics_datasource.dart'; + +/// Local in-memory transport to preview the Bonds module UI without backend connectivity. +class MockNseCbricsTransport implements HttpTransport { + final List> _orders = []; + + @override + Future get(String path, {Map? headers}) async { + if (path.endsWith('/bonds')) { + return jsonEncode({ + 'data': [ + { + 'isin': 'INE123A08018', + 'symbol': 'GSEC_2033_7.26', + 'issuer': 'Government of India', + 'maturityDate': '2033-04-06', + 'couponRate': 7.26, + 'faceValue': 100, + 'category': 'G-Sec', + }, + { + 'isin': 'INE987B07042', + 'symbol': 'SDL_MH_2031_7.10', + 'issuer': 'State of Maharashtra', + 'maturityDate': '2031-11-15', + 'couponRate': 7.10, + 'faceValue': 100, + 'category': 'SDL', + }, + ], + }); + } + + if (path.endsWith('/orders/open')) { + return jsonEncode({'data': _orders}); + } + + throw UnsupportedError('GET not supported in mock transport for path: $path'); + } + + @override + Future post( + String path, { + Map? headers, + Object? body, + }) async { + if (!path.endsWith('/orders')) { + throw UnsupportedError('POST not supported in mock transport for path: $path'); + } + + final request = jsonDecode(body as String) as Map; + final order = { + 'orderId': 'MOCK-${_orders.length + 1}', + 'isin': request['isin'], + 'side': request['side'], + 'type': request['type'], + 'status': 'OPEN', + 'quantity': request['quantity'], + 'filledQuantity': 0, + 'limitPrice': request['limitPrice'], + 'createdAt': DateTime.now().toUtc().toIso8601String(), + }; + + _orders.insert(0, order); + return jsonEncode({'data': order}); + } +} diff --git a/lib/features/bonds/data/datasources/nse_cbrics_datasource.dart b/lib/features/bonds/data/datasources/nse_cbrics_datasource.dart new file mode 100644 index 0000000..b897b72 --- /dev/null +++ b/lib/features/bonds/data/datasources/nse_cbrics_datasource.dart @@ -0,0 +1,76 @@ +import 'dart:convert'; + +import '../models/bond_model.dart'; +import '../models/bond_order_model.dart'; +import '../../domain/entities/bond_order.dart'; + +abstract class HttpTransport { + Future get(String path, {Map? headers}); + + Future post( + String path, { + Map? headers, + Object? body, + }); +} + +class NseCbricsDataSource { + final String baseUrl; + final String apiKey; + final HttpTransport http; + + const NseCbricsDataSource({ + required this.baseUrl, + required this.apiKey, + required this.http, + }); + + Map get _headers => { + 'Content-Type': 'application/json', + 'X-API-KEY': apiKey, + }; + + Future> fetchBonds() async { + final raw = await http.get('$baseUrl/bonds', headers: _headers); + final payload = jsonDecode(raw) as Map; + final items = payload['data'] as List; + + return items + .map((item) => BondModel.fromJson(item as Map)) + .toList(); + } + + Future submitOrder({ + required String isin, + required BondOrderSide side, + required BondOrderType type, + required int quantity, + double? limitPrice, + }) async { + final raw = await http.post( + '$baseUrl/orders', + headers: _headers, + body: jsonEncode({ + 'isin': isin, + 'side': side.name.toUpperCase(), + 'type': type.name.toUpperCase(), + 'quantity': quantity, + if (limitPrice != null) 'limitPrice': limitPrice, + }), + ); + + final payload = jsonDecode(raw) as Map; + final orderMap = (payload['data'] ?? payload) as Map; + return BondOrderModel.fromJson(orderMap); + } + + Future> fetchOpenOrders() async { + final raw = await http.get('$baseUrl/orders/open', headers: _headers); + final payload = jsonDecode(raw) as Map; + final items = payload['data'] as List; + + return items + .map((item) => BondOrderModel.fromJson(item as Map)) + .toList(); + } +} diff --git a/lib/features/bonds/data/models/bond_model.dart b/lib/features/bonds/data/models/bond_model.dart new file mode 100644 index 0000000..be00268 --- /dev/null +++ b/lib/features/bonds/data/models/bond_model.dart @@ -0,0 +1,25 @@ +import '../../domain/entities/bond.dart'; + +class BondModel extends Bond { + const BondModel({ + required super.isin, + required super.symbol, + required super.issuer, + required super.maturityDate, + required super.couponRate, + required super.faceValue, + required super.category, + }); + + factory BondModel.fromJson(Map json) { + return BondModel( + isin: json['isin'] as String, + symbol: json['symbol'] as String, + issuer: json['issuer'] as String, + maturityDate: DateTime.parse(json['maturityDate'] as String), + couponRate: (json['couponRate'] as num).toDouble(), + faceValue: (json['faceValue'] as num).toDouble(), + category: json['category'] as String, + ); + } +} diff --git a/lib/features/bonds/data/models/bond_order_model.dart b/lib/features/bonds/data/models/bond_order_model.dart new file mode 100644 index 0000000..4bb6cbf --- /dev/null +++ b/lib/features/bonds/data/models/bond_order_model.dart @@ -0,0 +1,70 @@ +import '../../domain/entities/bond_order.dart'; + +class BondOrderModel extends BondOrder { + const BondOrderModel({ + required super.orderId, + required super.isin, + required super.side, + required super.type, + required super.status, + required super.quantity, + required super.filledQuantity, + super.limitPrice, + required super.createdAt, + }); + + factory BondOrderModel.fromJson(Map json) { + return BondOrderModel( + orderId: json['orderId'] as String, + isin: json['isin'] as String, + side: _decodeSide(json['side'] as String), + type: _decodeType(json['type'] as String), + status: _decodeStatus((json['status'] as String?) ?? 'PENDING'), + quantity: (json['quantity'] as num).toInt(), + filledQuantity: ((json['filledQuantity'] as num?) ?? 0).toInt(), + limitPrice: (json['limitPrice'] as num?)?.toDouble(), + createdAt: DateTime.parse(json['createdAt'] as String), + ); + } + + static BondOrderSide _decodeSide(String side) { + switch (side.toUpperCase()) { + case 'BUY': + return BondOrderSide.buy; + case 'SELL': + return BondOrderSide.sell; + default: + throw ArgumentError.value(side, 'side', 'Unsupported side'); + } + } + + static BondOrderType _decodeType(String type) { + switch (type.toUpperCase()) { + case 'MARKET': + return BondOrderType.market; + case 'LIMIT': + return BondOrderType.limit; + default: + throw ArgumentError.value(type, 'type', 'Unsupported order type'); + } + } + + static BondOrderStatus _decodeStatus(String status) { + switch (status.toUpperCase()) { + case 'PENDING': + return BondOrderStatus.pending; + case 'OPEN': + return BondOrderStatus.open; + case 'PARTIALLY_FILLED': + return BondOrderStatus.partiallyFilled; + case 'FILLED': + return BondOrderStatus.filled; + case 'CANCELLED': + return BondOrderStatus.cancelled; + case 'REJECTED': + return BondOrderStatus.rejected; + default: + return BondOrderStatus.pending; + } + } +} diff --git a/lib/features/bonds/data/repositories/bond_repository_impl.dart b/lib/features/bonds/data/repositories/bond_repository_impl.dart new file mode 100644 index 0000000..7ed6506 --- /dev/null +++ b/lib/features/bonds/data/repositories/bond_repository_impl.dart @@ -0,0 +1,33 @@ +import '../../domain/entities/bond.dart'; +import '../../domain/entities/bond_order.dart'; +import '../../domain/repositories/bond_repository.dart'; +import '../datasources/nse_cbrics_datasource.dart'; + +class BondRepositoryImpl implements BondRepository { + final NseCbricsDataSource _dataSource; + + const BondRepositoryImpl(this._dataSource); + + @override + Future> getAvailableBonds() => _dataSource.fetchBonds(); + + @override + Future> getOpenOrders() => _dataSource.fetchOpenOrders(); + + @override + Future placeOrder({ + required String isin, + required BondOrderSide side, + required BondOrderType type, + required int quantity, + double? limitPrice, + }) { + return _dataSource.submitOrder( + isin: isin, + side: side, + type: type, + quantity: quantity, + limitPrice: limitPrice, + ); + } +} diff --git a/lib/features/bonds/domain/entities/bond.dart b/lib/features/bonds/domain/entities/bond.dart new file mode 100644 index 0000000..e9cee5d --- /dev/null +++ b/lib/features/bonds/domain/entities/bond.dart @@ -0,0 +1,19 @@ +class Bond { + final String isin; + final String symbol; + final String issuer; + final DateTime maturityDate; + final double couponRate; + final double faceValue; + final String category; + + const Bond({ + required this.isin, + required this.symbol, + required this.issuer, + required this.maturityDate, + required this.couponRate, + required this.faceValue, + required this.category, + }); +} diff --git a/lib/features/bonds/domain/entities/bond_order.dart b/lib/features/bonds/domain/entities/bond_order.dart new file mode 100644 index 0000000..8041304 --- /dev/null +++ b/lib/features/bonds/domain/entities/bond_order.dart @@ -0,0 +1,29 @@ +enum BondOrderSide { buy, sell } + +enum BondOrderType { limit, market } + +enum BondOrderStatus { pending, open, partiallyFilled, filled, cancelled, rejected } + +class BondOrder { + final String orderId; + final String isin; + final BondOrderSide side; + final BondOrderType type; + final BondOrderStatus status; + final int quantity; + final int filledQuantity; + final double? limitPrice; + final DateTime createdAt; + + const BondOrder({ + required this.orderId, + required this.isin, + required this.side, + required this.type, + required this.status, + required this.quantity, + required this.filledQuantity, + this.limitPrice, + required this.createdAt, + }); +} diff --git a/lib/features/bonds/domain/repositories/bond_repository.dart b/lib/features/bonds/domain/repositories/bond_repository.dart new file mode 100644 index 0000000..fc40a1f --- /dev/null +++ b/lib/features/bonds/domain/repositories/bond_repository.dart @@ -0,0 +1,16 @@ +import '../entities/bond.dart'; +import '../entities/bond_order.dart'; + +abstract class BondRepository { + Future> getAvailableBonds(); + + Future placeOrder({ + required String isin, + required BondOrderSide side, + required BondOrderType type, + required int quantity, + double? limitPrice, + }); + + Future> getOpenOrders(); +} diff --git a/lib/features/bonds/domain/usecases/place_bond_order.dart b/lib/features/bonds/domain/usecases/place_bond_order.dart new file mode 100644 index 0000000..e5c9267 --- /dev/null +++ b/lib/features/bonds/domain/usecases/place_bond_order.dart @@ -0,0 +1,36 @@ +import '../entities/bond_order.dart'; +import '../repositories/bond_repository.dart'; + +class PlaceBondOrder { + final BondRepository _repository; + + const PlaceBondOrder(this._repository); + + Future call({ + required String isin, + required BondOrderSide side, + required BondOrderType type, + required int quantity, + double? limitPrice, + }) { + if (isin.trim().isEmpty) { + throw ArgumentError.value(isin, 'isin', 'ISIN is required'); + } + + if (quantity <= 0) { + throw ArgumentError.value(quantity, 'quantity', 'Quantity must be positive'); + } + + if (type == BondOrderType.limit && (limitPrice == null || limitPrice <= 0)) { + throw ArgumentError.value(limitPrice, 'limitPrice', 'Limit price must be provided for limit orders'); + } + + return _repository.placeOrder( + isin: isin, + side: side, + type: type, + quantity: quantity, + limitPrice: type == BondOrderType.market ? null : limitPrice, + ); + } +} diff --git a/lib/features/bonds/presentation/controllers/bond_controller.dart b/lib/features/bonds/presentation/controllers/bond_controller.dart new file mode 100644 index 0000000..4d4aa2c --- /dev/null +++ b/lib/features/bonds/presentation/controllers/bond_controller.dart @@ -0,0 +1,31 @@ +import '../../domain/entities/bond.dart'; +import '../../domain/entities/bond_order.dart'; +import '../../domain/repositories/bond_repository.dart'; +import '../../domain/usecases/place_bond_order.dart'; + +class BondController { + final BondRepository _repository; + late final PlaceBondOrder _placeBondOrder; + + BondController(this._repository) { + _placeBondOrder = PlaceBondOrder(_repository); + } + + Future> loadBonds() => _repository.getAvailableBonds(); + + Future> loadOpenOrders() => _repository.getOpenOrders(); + + Future placeLimitBuyOrder({ + required String isin, + required int quantity, + required double price, + }) { + return _placeBondOrder( + isin: isin, + side: BondOrderSide.buy, + type: BondOrderType.limit, + quantity: quantity, + limitPrice: price, + ); + } +} diff --git a/lib/features/bonds/presentation/pages/bond_page.dart b/lib/features/bonds/presentation/pages/bond_page.dart new file mode 100644 index 0000000..d633d8f --- /dev/null +++ b/lib/features/bonds/presentation/pages/bond_page.dart @@ -0,0 +1,95 @@ +import 'package:flutter/material.dart'; + +import '../../domain/entities/bond.dart'; +import '../controllers/bond_controller.dart'; +import '../widgets/bond_order_ticket.dart'; + +class BondPage extends StatefulWidget { + final BondController controller; + + const BondPage({super.key, required this.controller}); + + @override + State createState() => _BondPageState(); +} + +class _BondPageState extends State { + late Future> _bondsFuture; + + @override + void initState() { + super.initState(); + _bondsFuture = widget.controller.loadBonds(); + } + + Future _refresh() async { + setState(() { + _bondsFuture = widget.controller.loadBonds(); + }); + await _bondsFuture; + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: const Text('Indian Bonds (NSE CBRICS)')), + body: RefreshIndicator( + onRefresh: _refresh, + child: FutureBuilder>( + future: _bondsFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Center(child: CircularProgressIndicator()); + } + + if (snapshot.hasError) { + return ListView( + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: Text('Failed to load bonds: ${snapshot.error}'), + ), + ], + ); + } + + final bonds = snapshot.data ?? const []; + if (bonds.isEmpty) { + return ListView( + children: const [ + Padding( + padding: EdgeInsets.all(16), + child: Text('No bonds available'), + ), + ], + ); + } + + return ListView.builder( + itemCount: bonds.length, + itemBuilder: (context, index) { + final bond = bonds[index]; + return ExpansionTile( + title: Text('${bond.symbol} (${bond.isin})'), + subtitle: Text( + '${bond.issuer} • Coupon ${bond.couponRate.toStringAsFixed(2)}% • Maturity ${bond.maturityDate.toIso8601String().split('T').first}', + ), + trailing: Text('₹${bond.faceValue.toStringAsFixed(0)}'), + children: [ + Padding( + padding: const EdgeInsets.fromLTRB(12, 0, 12, 12), + child: BondOrderTicket( + controller: widget.controller, + isin: bond.isin, + ), + ), + ], + ); + }, + ); + }, + ), + ), + ); + } +} diff --git a/lib/features/bonds/presentation/pages/bond_preview_page.dart b/lib/features/bonds/presentation/pages/bond_preview_page.dart new file mode 100644 index 0000000..99567cf --- /dev/null +++ b/lib/features/bonds/presentation/pages/bond_preview_page.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; + +import '../../data/datasources/mock_nse_cbrics_transport.dart'; +import '../../data/datasources/nse_cbrics_datasource.dart'; +import '../../data/repositories/bond_repository_impl.dart'; +import '../controllers/bond_controller.dart'; +import 'bond_page.dart'; + +/// Drop-in page to preview the Bonds module with mock market data. +class BondPreviewPage extends StatelessWidget { + const BondPreviewPage({super.key}); + + @override + Widget build(BuildContext context) { + final dataSource = NseCbricsDataSource( + baseUrl: 'https://mock.nse-cbrics.local', + apiKey: 'preview-key', + http: MockNseCbricsTransport(), + ); + + final repository = BondRepositoryImpl(dataSource); + final controller = BondController(repository); + + return BondPage(controller: controller); + } +} diff --git a/lib/features/bonds/presentation/widgets/bond_order_ticket.dart b/lib/features/bonds/presentation/widgets/bond_order_ticket.dart new file mode 100644 index 0000000..d6e7571 --- /dev/null +++ b/lib/features/bonds/presentation/widgets/bond_order_ticket.dart @@ -0,0 +1,92 @@ +import 'package:flutter/material.dart'; + +import '../controllers/bond_controller.dart'; + +class BondOrderTicket extends StatefulWidget { + final BondController controller; + final String isin; + + const BondOrderTicket({ + super.key, + required this.controller, + required this.isin, + }); + + @override + State createState() => _BondOrderTicketState(); +} + +class _BondOrderTicketState extends State { + final _qtyController = TextEditingController(text: '10'); + final _priceController = TextEditingController(text: '100.00'); + bool _submitting = false; + String? _message; + + @override + void dispose() { + _qtyController.dispose(); + _priceController.dispose(); + super.dispose(); + } + + Future _submit() async { + setState(() { + _submitting = true; + _message = null; + }); + + try { + final qty = int.parse(_qtyController.text.trim()); + final price = double.parse(_priceController.text.trim()); + final order = await widget.controller.placeLimitBuyOrder( + isin: widget.isin, + quantity: qty, + price: price, + ); + + setState(() { + _message = 'Order placed: ${order.orderId}'; + }); + } catch (e) { + setState(() { + _message = 'Order failed: $e'; + }); + } finally { + if (mounted) { + setState(() => _submitting = false); + } + } + } + + @override + Widget build(BuildContext context) { + return Card( + child: Padding( + padding: const EdgeInsets.all(12), + child: Column( + children: [ + TextField( + controller: _qtyController, + keyboardType: TextInputType.number, + decoration: const InputDecoration(labelText: 'Quantity'), + ), + TextField( + controller: _priceController, + keyboardType: const TextInputType.numberWithOptions(decimal: true), + decoration: const InputDecoration(labelText: 'Limit Price'), + ), + const SizedBox(height: 8), + ElevatedButton( + onPressed: _submitting ? null : _submit, + child: Text(_submitting ? 'Placing...' : 'Place Buy Order'), + ), + if (_message != null) ...[ + const SizedBox(height: 8), + Text(_message!), + ], + ], + ), + ), + ); + } +} diff --git a/lib/main.dart b/lib/main.dart new file mode 100644 index 0000000..6d14ffa --- /dev/null +++ b/lib/main.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; + +import 'features/bonds/presentation/pages/bond_preview_page.dart'; + +void main() { + runApp(const BondDemoApp()); +} + +class BondDemoApp extends StatelessWidget { + const BondDemoApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Indian Bonds Preview', + debugShowCheckedModeBanner: false, + theme: ThemeData( + colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo), + useMaterial3: true, + ), + home: const BondPreviewPage(), + ); + } +} diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..618cbef --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,18 @@ +name: test_web +publish_to: 'none' +version: 0.1.0+1 + +environment: + sdk: '>=3.3.0 <4.0.0' + +dependencies: + flutter: + sdk: flutter + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^5.0.0 + +flutter: + uses-material-design: true