Contributing to MindPaystack
Thank you for your interest in contributing to MindPaystack! This guide will help you get started and ensure your contributions align with our project standards.
Quick Start
Prerequisites
Before contributing, ensure you have:
- Dart SDK: Version 3.5.0 or higher
- Git: For version control
- Melos: For monorepo management
- IDE/Editor: VS Code, IntelliJ, or your preferred Dart IDE
First-Time Setup
# 1. Fork and clone the repository
git clone https://github.com/Dartmind-OpenSource/mind-paystack.git
cd mind-paystack
# 2. Install Melos globally (if not already installed)
dart pub global activate melos
# 3. Bootstrap the project
melos bootstrap
# 4. Verify setup
melos run test
melos run analyze
Project Structure
mind-paystack/
├── packages/
│ └── mind_paystack/ # Core SDK package
├── apps/
│ ├── ex00_checkout_basic/ # Dart CLI example
│ └── ex01_checkout_flutter/ # Flutter app example
├── docs/ # Documentation site
├── melos.yaml # Monorepo configuration
└── README.md # Project overview
Development Workflow
1. Create a Feature Branch
git checkout -b feature/your-feature-name
# or
git checkout -b fix/issue-description
2. Make Your Changes
- Follow our code standards
- Write tests for new functionality
- Update documentation as needed
- Test across all relevant packages
3. Test Your Changes
# Run all tests across the monorepo
melos run test
# Run tests for specific package
melos run test --scope=mind_paystack
# Analyze code
melos run analyze
# Format code
melos run format
4. Commit and Push
git add .
git commit -m "feat: add your feature description"
git push origin feature/your-feature-name
Code Standards
Dart Style Guidelines
We follow the official Dart Style Guide and Effective Dart .
Formatting
- Use
dart format
to format your code - Line length: 80 characters maximum
- Use 2 spaces for indentation
- Trailing commas for better diffs
Naming Conventions
// Classes: PascalCase
class PaystackTransaction {}
// Functions and variables: camelCase
void initializePayment() {}
final String transactionReference = 'txn_123';
// Constants: lowerCamelCase with const
const String defaultCurrency = 'NGN';
const int maxRetryAttempts = 3;
// Private members: leading underscore
String _buildAuthHeader() {}
Documentation Standards
/// Initializes a payment transaction with Paystack.
///
/// This method creates a new transaction and returns the authorization URL
/// that customers can use to complete their payment. The transaction will
/// be created with the specified [amount] and [email].
///
/// Example:
/// ```dart
/// final transaction = await PaystackAPI.initializeTransaction(
/// amount: 50000, // ₦500.00 in kobo
/// email: 'customer@example.com',
/// );
/// print('Payment URL: ${transaction.authorizationUrl}');
/// ```
///
/// [amount] The payment amount in kobo (required, must be positive)
/// [email] The customer's email address (required, must be valid)
/// [currency] The payment currency (optional, defaults to 'NGN')
/// [reference] Custom transaction reference (optional, auto-generated if null)
/// [metadata] Additional transaction metadata (optional)
///
/// Returns a [PaystackTransaction] containing the authorization URL and details.
///
/// Throws [PaystackException] if the API request fails.
/// Throws [ValidationException] if the input parameters are invalid.
Future<PaystackTransaction> initializeTransaction({
required int amount,
required String email,
String currency = 'NGN',
String? reference,
Map<String, dynamic>? metadata,
}) async {
// Implementation
}
Error Handling
try {
final result = await paystackApi.call();
return result;
} on PaystackException catch (e) {
// Handle Paystack-specific errors
_logger.error('Paystack API error: ${e.message}', e);
rethrow;
} on HttpException catch (e) {
// Handle network errors
_logger.error('Network error: ${e.message}', e);
throw PaystackException('Network request failed: ${e.message}');
} catch (e) {
// Handle unexpected errors
_logger.error('Unexpected error: $e', e);
throw PaystackException('An unexpected error occurred');
}
Flutter Specific Guidelines
Widget Structure
class PaymentButton extends StatefulWidget {
/// Creates a payment button widget.
const PaymentButton({
super.key,
required this.amount,
required this.onPaymentSuccess,
this.onPaymentError,
this.currency = 'NGN',
});
final int amount;
final VoidCallback onPaymentSuccess;
final void Function(String error)? onPaymentError;
final String currency;
@override
State<PaymentButton> createState() => _PaymentButtonState();
}
State Management
- Use
StatefulWidget
for local state - Use appropriate state management solution for complex state
- Always dispose of resources in
dispose()
Testing
Testing Philosophy
- Write tests first (TDD approach preferred)
- Test behavior, not implementation
- Cover happy path, edge cases, and error scenarios
- Mock external dependencies (HTTP requests, file system, etc.)
Test Structure
void main() {
group('PaystackAPI', () {
late PaystackAPI api;
late MockHttpClient mockClient;
setUp(() {
mockClient = MockHttpClient();
api = PaystackAPI(httpClient: mockClient);
});
group('initializeTransaction', () {
test('should return transaction when API call succeeds', () async {
// Arrange
when(() => mockClient.post(any(), body: any(named: 'body')))
.thenAnswer((_) async => Response(
'{"status": true, "data": {"authorization_url": "https://checkout.paystack.com/xyz"}}',
200,
));
// Act
final result = await api.initializeTransaction(
amount: 50000,
email: 'test@example.com',
);
// Assert
expect(result.authorizationUrl, equals('https://checkout.paystack.com/xyz'));
verify(() => mockClient.post(
Uri.parse('https://api.paystack.co/transaction/initialize'),
body: any(named: 'body'),
)).called(1);
});
test('should throw PaystackException when API returns error', () async {
// Arrange
when(() => mockClient.post(any(), body: any(named: 'body')))
.thenAnswer((_) async => Response(
'{"status": false, "message": "Invalid email"}',
400,
));
// Act & Assert
expect(
() => api.initializeTransaction(
amount: 50000,
email: 'invalid-email',
),
throwsA(isA<PaystackException>()
.having((e) => e.message, 'message', contains('Invalid email'))),
);
});
group('input validation', () {
test('should throw ValidationException for negative amount', () {
expect(
() => api.initializeTransaction(
amount: -100,
email: 'test@example.com',
),
throwsA(isA<ValidationException>()),
);
});
test('should throw ValidationException for invalid email', () {
expect(
() => api.initializeTransaction(
amount: 50000,
email: 'not-an-email',
),
throwsA(isA<ValidationException>()),
);
});
});
});
});
}
Running Tests
# Run all tests in the monorepo
melos run test
# Run tests with coverage
melos run test:coverage
# Run tests for specific package
melos run test --scope=mind_paystack
# Run specific test file
cd packages/mind_paystack
dart test test/api/paystack_api_test.dart
# Run tests with specific name pattern
dart test --name "initializeTransaction"
Test Coverage Requirements
- Minimum 80% code coverage for new code
- 100% coverage for critical payment flows
- Integration tests for end-to-end scenarios
- Widget tests for Flutter components
Package Development
Core SDK Package (packages/mind_paystack
)
This is the main SDK package. When contributing:
- API Changes: Follow semantic versioning
- Breaking Changes: Document in CHANGELOG.md
- Dependencies: Keep minimal and well-justified
- Platform Support: Ensure cross-platform compatibility
Example Applications
Dart CLI Example (apps/ex00_checkout_basic
)
- Demonstrates basic SDK usage
- Should work with latest SDK version
- Include comprehensive error handling
- Provide clear console output
Flutter Example (apps/ex01_checkout_flutter
)
- Showcase UI integration patterns
- Demonstrate best practices
- Handle platform-specific considerations
- Include proper state management
Pull Request Process
Before Submitting
- Tests pass:
melos run test
- Code analysis passes:
melos run analyze
- Code is formatted:
melos run format
- Documentation updated: Relevant docs and README files
- Examples work: Test example applications
- CHANGELOG updated: For significant changes
PR Checklist Template
## Summary
Brief description of the changes and their purpose.
## Type of Change
- [ ] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that causes existing functionality to not work as expected)
- [ ] Documentation update
- [ ] Refactoring (no functional changes)
- [ ] Test improvements
## Testing
- [ ] Unit tests added/updated
- [ ] Integration tests added/updated
- [ ] Manual testing performed
- [ ] Example applications tested
## Documentation
- [ ] Code comments added/updated
- [ ] API documentation updated
- [ ] README updated (if applicable)
- [ ] CHANGELOG.md updated
## Screenshots (if applicable)
<!-- Add screenshots for UI changes -->
## Related Issues
<!-- Link to related issues -->
Closes #123
References #456
## Additional Notes
<!-- Any additional information for reviewers -->
Review Process
- Automated Checks: GitHub Actions must pass
- Code Review: At least one maintainer approval
- Testing: Verify functionality in examples
- Documentation: Ensure docs are comprehensive
- Performance: Check for any performance regressions
Release Process
Version Numbering
We follow Semantic Versioning :
- PATCH (1.0.1): Bug fixes, documentation updates
- MINOR (1.1.0): New features, non-breaking changes
- MAJOR (2.0.0): Breaking changes
Release Steps
# 1. Update version numbers
melos version
# 2. Update CHANGELOG.md
# Add entry for new version
# 3. Create release commit
git add .
git commit -m "chore: release v1.2.0"
# 4. Create and push tag
git tag v1.2.0
git push origin main --tags
# 5. Publish to pub.dev (maintainers only)
melos publish
Security Guidelines
API Key Security
// ❌ Never hardcode API keys
const String secretKey = 'sk_test_your_key_here';
// âś… Always use environment variables or secure storage
final String secretKey = Platform.environment['PAYSTACK_SECRET_KEY'] ??
throw Exception('PAYSTACK_SECRET_KEY not found');
Input Validation
// Always validate user inputs
void validateEmail(String email) {
if (!RegExp(r'^[^@]+@[^@]+\.[^@]+$').hasMatch(email)) {
throw ValidationException('Invalid email format');
}
}
void validateAmount(int amount) {
if (amount <= 0) {
throw ValidationException('Amount must be positive');
}
if (amount > 100000000) { // 1 million naira in kobo
throw ValidationException('Amount exceeds maximum limit');
}
}
Error Information
// ❌ Don't expose sensitive information in errors
throw Exception('Database connection failed: password=secret123');
// âś… Provide helpful but secure error messages
throw PaystackException('Unable to process payment. Please try again.');
Documentation Guidelines
API Documentation
- Every public method must have comprehensive documentation
- Include examples that actually work
- Document exceptions that can be thrown
- Explain parameters and return values clearly
Code Examples
/// Example of good code documentation:
///
/// ```dart
/// // Initialize the SDK
/// await MindPaystack.initialize(PaystackConfig(
/// publicKey: 'pk_test_your_key',
/// secretKey: 'sk_test_your_key',
/// environment: Environment.test,
/// ));
///
/// // Create a transaction
/// final transaction = await MindPaystack.instance.transaction.initialize(
/// InitializeTransactionOptions(
/// amount: '50000', // ₦500.00 in kobo
/// email: 'customer@example.com',
/// ),
/// );
///
/// print('Payment URL: ${transaction.data?.authorizationUrl}');
/// ```
README Updates
When adding new features, update:
- Feature list in main README
- Installation instructions (if needed)
- Quick start guide
- Example applications
Development Tools
Recommended VS Code Settings
Create .vscode/settings.json
:
{
"dart.flutterSdkPath": "/path/to/flutter",
"dart.lineLength": 80,
"editor.rulers": [80],
"editor.formatOnSave": true,
"dart.runPubGetOnPubspecChanges": true,
"files.associations": {
"melos.yaml": "yaml"
}
}
Useful Melos Commands
# Bootstrap all packages
melos bootstrap
# Run tests across all packages
melos run test
# Run analysis across all packages
melos run analyze
# Format code across all packages
melos run format
# Clean all packages
melos clean
# List all packages
melos list
# Run command in specific package
melos exec --scope=mind_paystack -- dart test
Git Hooks (Optional)
Create .git/hooks/pre-commit
:
#!/bin/bash
echo "Running pre-commit checks..."
# Format code
echo "Formatting code..."
melos run format
# Analyze code
echo "Analyzing code..."
melos run analyze --no-fatal-infos
# Run tests
echo "Running tests..."
melos run test
echo "Pre-commit checks passed!"
Make it executable:
chmod +x .git/hooks/pre-commit
Troubleshooting
Common Issues
Melos Bootstrap Fails
# Clear pub cache and retry
dart pub cache clean
melos clean
melos bootstrap
Tests Fail in CI but Pass Locally
- Check Dart SDK version consistency
- Verify all dependencies are committed
- Check for platform-specific test failures
Import Errors
- Run
melos bootstrap
after adding new dependencies - Check relative import paths
- Verify package names in pubspec.yaml
Flutter Example Issues
- Ensure Flutter SDK is up to date
- Check platform-specific configurations
- Verify plugin dependencies
Getting Help
- Search existing issues for similar problems
- Check documentation and examples
- Create a new issue with:
- Clear problem description
- Steps to reproduce
- Expected vs actual behavior
- Environment details (Dart/Flutter versions, OS)
- Minimal code example
Community
Communication Channels
- GitHub Issues: Bug reports and feature requests
- GitHub Discussions: Questions and community chat
- Pull Requests: Code contributions and reviews
Code of Conduct
We’re committed to fostering an inclusive and welcoming community:
- Be respectful and considerate in all interactions
- Be collaborative and help others learn and grow
- Be patient with newcomers and different perspectives
- Give constructive feedback and accept it gracefully
- Focus on what’s best for the community and project
Recognition
Contributors are recognized through:
- GitHub contributor graphs
- Release notes and changelogs
- Special mentions for significant contributions
Performance Considerations
API Efficiency
// ❌ Don't make unnecessary API calls
for (final transaction in transactions) {
await api.verifyTransaction(transaction.reference);
}
// âś… Batch operations when possible
final verifications = await Future.wait(
transactions.map((t) => api.verifyTransaction(t.reference)),
);
Memory Management
// âś… Dispose of streams and subscriptions
class PaymentService {
StreamSubscription? _subscription;
void dispose() {
_subscription?.cancel();
}
}
Error Recovery
// âś… Implement retry logic for transient failures
Future<T> withRetry<T>(Future<T> Function() operation, {
int maxAttempts = 3,
Duration delay = const Duration(seconds: 1),
}) async {
for (int attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await operation();
} catch (e) {
if (attempt == maxAttempts || !_isRetryableError(e)) rethrow;
await Future.delayed(delay * attempt);
}
}
throw StateError('This should never be reached');
}
Thank You!
Thank you for contributing to MindPaystack! Your efforts help make payment processing easier and more reliable for developers across Africa and beyond.
Every contribution, whether it’s a bug fix, new feature, documentation improvement, or even a typo correction, makes a difference. We appreciate your time and effort in helping make this project better.
Happy coding!