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.
- Code of Conduct
- Getting Started
- How Can I Contribute?
- Development Workflow
- Coding Standards
- Community
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.
-
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
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)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
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 handlersCommit 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
-
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
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 }
- 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
}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)))
}
}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
// 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
}- Discussions - Join GitHub Discussions
- Issues - Track open issues
- Pull Requests - Review open PRs
Contributors are recognized in:
- GitHub contributors page
- Release notes
- Project README (for significant contributions)
- Check existing issues
- Search discussions
- Ask in Q&A discussions
- Email the maintainer: nv3212@gmail.com
- 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.