Contributing to Flare
February 19, 2026 · View on GitHub
First off, thank you for considering contributing to Flare! It's people like you that make Flare such a great tool for working with in-app purchases.
Table of Contents
- Code of Conduct
- Getting Started
- How Can I Contribute?
- Development Workflow
- Coding Standards
- Community
Code of Conduct
This project and everyone participating in it is governed by our Code of Conduct. By participating, you are expected to uphold this code. Please report unacceptable behavior to nv3212@gmail.com.
Getting Started
Development Setup
-
Fork the repository
# Click the "Fork" button on GitHub -
Clone your fork
git clone https://github.com/YOUR_USERNAME/flare.git cd flare -
Set up the development environment
# Install mise (if not already installed) curl https://mise.run | sh # Install project dependencies mise install -
Create a feature branch
git checkout -b feature/your-feature-name -
Open the project in Xcode
open Package.swift
How Can I Contribute?
Reporting Bugs
Before creating a bug report, please check the existing issues to avoid duplicates.
When creating a bug report, use our bug report template and include:
- Clear title - Describe the issue concisely
- Reproduction steps - Detailed steps to reproduce the bug
- Expected behavior - What you expected to happen
- Actual behavior - What actually happened
- Environment - OS, Xcode version, Swift version, StoreKit version
- Code samples - Minimal reproducible example
- Console logs - Relevant error messages or logs
- Screenshots - If applicable (especially for UI issues)
Example:
**Title:** Purchase completion handler not called for cancelled transactions
**Steps to reproduce:**
1. Call Flare.shared.purchase(product: product)
2. Cancel the purchase dialog
3. Completion handler is never invoked
**Expected:** Completion called with .cancelled result
**Actual:** No callback, causing UI to hang
**Environment:**
- iOS 17.0
- Xcode 15.3
- Flare 3.1.0
- Testing on physical device (iPhone 15 Pro)
Suggesting Features
We love feature suggestions! Use our feature request template and include:
- Problem statement - What problem does this solve?
- Proposed solution - How should it work?
- Alternatives - What alternatives did you consider?
- Use cases - Real-world scenarios where this would be useful
- API design - Example code showing proposed usage
- StoreKit compatibility - Consider both StoreKit 1 and 2
Example:
**Problem:** Difficult to track subscription renewal dates
**Proposed solution:**
Add a method to get the next renewal date for active subscriptions:
```swift
let renewalDate = try await Flare.shared.renewalDate(for: subscription)
Use cases:
- Display "Next billing date" in subscription management UI
- Send reminders before renewal
- Analytics on subscription lifecycle
### Improving Documentation
Documentation improvements are always welcome:
- **Code comments** - Add/improve inline documentation
- **API documentation** - Enhance DocC documentation
- **README** - Fix typos, add examples, improve clarity
- **Guides** - Write tutorials or how-to guides
- **Code examples** - Add sample code for common use cases
- **Translations** - Help translate documentation
Use our [documentation template](.github/ISSUE_TEMPLATE/documentation.md) for documentation issues.
### Submitting Code
1. **Check existing work** - Look for related issues or PRs
2. **Discuss major changes** - Open an issue for large features
3. **Follow coding standards** - See [Coding Standards](#coding-standards)
4. **Write tests** - All code changes require tests
5. **Update documentation** - Keep docs in sync with code
6. **Test on devices** - Test purchases on physical devices when possible
7. **Create a pull request** - Use our [PR template](.github/PULL_REQUEST_TEMPLATE.md)
## Development Workflow
### Branching Strategy
We follow a simplified Git Flow:
- **`main`** - Main development branch (default, all PRs target this branch)
- **`feature/*`** - New features
- **`fix/*`** - Bug fixes
- **`docs/*`** - Documentation updates
- **`refactor/*`** - Code refactoring
- **`test/*`** - Test improvements
**Branch naming examples:**
```bash
feature/promotional-offers-support
fix/transaction-finish-callback
docs/update-swiftui-examples
refactor/storekit2-implementation
test/add-subscription-tests
Commit Guidelines
We use Conventional Commits for clear, structured commit history.
Format:
<type>(<scope>): <subject>
<body>
<footer>
Types:
feat- New featurefix- Bug fixdocs- Documentation changesstyle- Code style (formatting, missing semicolons)refactor- Code refactoringtest- Adding or updating testschore- Maintenance tasks (dependencies, tooling)perf- Performance improvementsci- CI/CD changes
Scopes:
core- Core Flare framework changesui- FlareUI changesstorekit1- StoreKit 1 implementationstorekit2- StoreKit 2 implementationpurchases- Purchase handling logicsubscriptions- Subscription-specific featurestransactions- Transaction managementswiftui- SwiftUI componentsuikit- UIKit componentsdeps- Dependencies
Examples:
feat(subscriptions): add support for promotional offers
Implement promotional offer support for subscriptions including:
- Offer signature validation
- Offer redemption flow
- Error handling for invalid offers
Closes #123
---
fix(transactions): handle finish completion callback correctly
Previously, the finish completion handler was not called when
transactions failed to finish. Now properly invokes callback
with error result.
Fixes #456
---
docs(swiftui): add subscription management examples
Add comprehensive examples for subscription management including:
- Displaying active subscriptions
- Handling subscription upgrades/downgrades
- Restoring purchases
---
test(purchases): increase coverage for purchase flow
Add tests for:
- Cancelled purchases
- Failed purchases with various error codes
- Pending purchases (Ask to Buy)
- Purchase restoration
---
refactor(core): modernize async/await implementation
Replace completion handler based APIs with async/await
for better readability and error handling.
BREAKING CHANGE: Some APIs now use async/await instead of completion handlers
Commit message rules:
- Use imperative mood ("add" not "added")
- Don't capitalize first letter
- No period at the end
- Keep subject line under 72 characters
- Separate subject from body with blank line
- Reference issues in footer
- Mark breaking changes with
BREAKING CHANGE:in footer
Pull Request Process
-
Update your branch
git checkout main git pull upstream main git checkout feature/your-feature git rebase main -
Run tests and checks
# Run all tests swift test # Run specific test suite swift test --filter FlareTests # Check code formatting mise run lint # Build for all platforms swift build --platform ios swift build --platform macos swift build --platform tvos swift build --platform watchos -
Push to your fork
git push origin feature/your-feature -
Create pull request
- Use our PR template
- Target the
mainbranch - Link related issues
- Add screenshots/videos for UI changes
- Describe testing performed (devices, scenarios)
- Request review from maintainers
-
Review process
- Address review comments promptly
- Keep PR up to date with main branch
- Squash commits if requested
- Wait for all CI checks to pass
- Ensure test coverage meets requirements
-
After merge
# Clean up local branch git checkout main git pull upstream main git branch -d feature/your-feature # Clean up remote branch git push origin --delete feature/your-feature
Coding Standards
Swift Style Guide
We follow the Swift API Design Guidelines and Ray Wenderlich Swift Style Guide.
Key points:
-
Naming
// ✅ Good func purchase(product: Product) async throws -> PurchaseResult let isSubscriptionActive: Bool // ❌ Bad func buy(prod: Product) async throws -> Bool let active: Bool -
Protocols
// ✅ Good - Use "I" prefix for protocols protocol IFlareProvider { func products(productIDs: [String]) async throws -> [Product] func purchase(product: Product) async throws -> PurchaseResult } // ❌ Bad protocol FlareProvider { } -
Access Control
// ✅ Good - Explicit access control public final class Flare: IFlareProvider { private let storeKitProvider: IStoreKitProvider private var transactionObserver: TransactionObserver? public static let shared = Flare() private init() { self.storeKitProvider = StoreKitProvider() } } -
Async/Await
// ✅ Good - Use async/await for asynchronous operations public func purchase(product: Product) async throws -> PurchaseResult { let result = try await storeKitProvider.purchase(product) return result } // ❌ Bad - Don't use completion handlers for new code public func purchase(product: Product, completion: @escaping (Result<PurchaseResult, Error>) -> Void) { // ... } -
Documentation
/// Purchases a product from the App Store. /// /// This method handles the complete purchase flow including: /// - User authentication /// - Payment processing /// - Transaction verification /// /// - Parameters: /// - product: The product to purchase /// - promotionalOffer: Optional promotional offer to apply /// /// - Returns: The result of the purchase operation /// - Throws: `FlareError` if the purchase fails /// /// - Example: /// ```swift /// let product = try await Flare.shared.products(productIDs: ["premium"]).first! /// let result = try await Flare.shared.purchase(product: product) /// if case .purchased(let transaction) = result { /// print("Purchase successful!") /// } /// ``` /// /// - Important: Always call `finish(transaction:)` after successfully processing a purchase. /// - Note: This method must be called from the main thread. public func purchase( product: Product, promotionalOffer: PromotionalOffer? = nil ) async throws -> PurchaseResult { // Implementation }
Code Quality
- No force unwrapping - Use optional binding or guards
- No force casting - Use conditional casting
- No magic numbers - Use named constants
- Single responsibility - One class, one purpose
- DRY principle - Don't repeat yourself
- SOLID principles - Follow SOLID design
- Error handling - Always handle errors gracefully
- Thread safety - Ensure thread-safe access to shared resources
Example:
// ✅ Good
private enum Constants {
static let maxRetryAttempts = 3
static let retryDelay: TimeInterval = 1.0
}
public func fetchProducts(productIDs: [String]) async throws -> [Product] {
guard !productIDs.isEmpty else {
throw FlareError.invalidProductIDs
}
let products = try await storeKitProvider.products(for: productIDs)
guard !products.isEmpty else {
throw FlareError.productsNotFound
}
return products
}
// ❌ Bad
public func fetchProducts(productIDs: [String]) async throws -> [Product] {
let products = try await storeKitProvider.products(for: productIDs)
return products
}
Testing Requirements
All code changes must include comprehensive tests:
- Unit tests - Test individual components in isolation
- Integration tests - Test component interactions
- Edge cases - Test boundary conditions
- Error handling - Test all failure scenarios
- Snapshot tests - For UI components (FlareUI)
Coverage requirements:
- New code: minimum 80% coverage
- Modified code: maintain or improve existing coverage
- Critical paths (purchases, transactions): 100% coverage
Test structure:
import XCTest
@testable import Flare
final class PurchaseFlowTests: XCTestCase {
var sut: Flare!
var mockStoreKit: MockStoreKitProvider!
override func setUp() {
super.setUp()
mockStoreKit = MockStoreKitProvider()
sut = Flare(storeKitProvider: mockStoreKit)
}
override func tearDown() {
sut = nil
mockStoreKit = nil
super.tearDown()
}
// MARK: - Successful Purchase Tests
func testPurchase_WithValidProduct_ReturnsSuccessResult() async throws {
// Given
let product = Product.mock(id: "premium")
let expectedTransaction = Transaction.mock()
mockStoreKit.purchaseResult = .success(.purchased(expectedTransaction))
// When
let result = try await sut.purchase(product: product)
// Then
guard case .purchased(let transaction) = result else {
XCTFail("Expected purchased result")
return
}
XCTAssertEqual(transaction.id, expectedTransaction.id)
}
// MARK: - Cancelled Purchase Tests
func testPurchase_WhenUserCancels_ReturnsCancelledResult() async throws {
// Given
let product = Product.mock(id: "premium")
mockStoreKit.purchaseResult = .success(.cancelled)
// When
let result = try await sut.purchase(product: product)
// Then
guard case .cancelled = result else {
XCTFail("Expected cancelled result")
return
}
}
// MARK: - Error Handling Tests
func testPurchase_WithNetworkError_ThrowsError() async {
// Given
let product = Product.mock(id: "premium")
mockStoreKit.purchaseResult = .failure(.networkError)
// When/Then
do {
_ = try await sut.purchase(product: product)
XCTFail("Expected error to be thrown")
} catch let error as FlareError {
XCTAssertEqual(error, .networkError)
} catch {
XCTFail("Expected FlareError")
}
}
// MARK: - Edge Cases
func testPurchase_WithPendingTransaction_ReturnsPendingResult() async throws {
// Given
let product = Product.mock(id: "premium")
mockStoreKit.purchaseResult = .success(.pending)
// When
let result = try await sut.purchase(product: product)
// Then
guard case .pending = result else {
XCTFail("Expected pending result")
return
}
}
}
UI Testing (FlareUI):
import XCTest
import SnapshotTesting
@testable import FlareUI
final class ProductViewSnapshotTests: XCTestCase {
func testProductView_WithStandardProduct_MatchesSnapshot() {
// Given
let product = Product.mock(
id: "premium",
title: "Premium Subscription",
price: "\$9.99"
)
let view = ProductView(product: product)
// When/Then
assertSnapshot(matching: view, as: .image(layout: .device(config: .iPhone13)))
}
func testProductView_WithSubscription_ShowsSubscriptionDetails() {
// Given
let subscription = Product.mock(
id: "monthly",
title: "Monthly Subscription",
price: "\$4.99",
subscriptionPeriod: .month
)
let view = ProductView(product: subscription)
// When/Then
assertSnapshot(matching: view, as: .image(layout: .device(config: .iPhone13)))
}
}
Testing with StoreKit
StoreKit Configuration File
Use a StoreKit configuration file for testing:
- Create
Configuration.storekitin your test target - Add test products, subscriptions, and offers
- Configure in Xcode scheme for testing
Testing Purchases
// Use sandbox testing for integration tests
// Configure test products in Configuration.storekit
func testRealPurchaseFlow() async throws {
let productID = "test.premium.monthly"
let products = try await Flare.shared.products(productIDs: [productID])
guard let product = products.first else {
XCTFail("Test product not found")
return
}
let result = try await Flare.shared.purchase(product: product)
// Verify purchase result
}
Community
- Discussions - Join GitHub Discussions
- Issues - Track open issues
- Pull Requests - Review open PRs
Recognition
Contributors are recognized in:
- GitHub contributors page
- Release notes
- Project README (for significant contributions)
Questions?
- Check existing issues
- Search discussions
- Ask in Q&A discussions
- Email the maintainer: nv3212@gmail.com
Additional Resources
- StoreKit Documentation
- StoreKit 2 Documentation
- In-App Purchase Best Practices
- Testing In-App Purchases
Thank you for contributing to Flare! 🎉
Your efforts help make in-app purchases easier for developers everywhere.