Skip to content

Commit 8dc6a78

Browse files
JOHNJOHN
authored andcommitted
feat(paykit): implement background Noise server with push notifications and invoice number linking
- Add paykitNoiseRequest notification type to BlocktankNotificationType - Update FcmService to handle Noise request notifications - Create NoiseServerWorker for background Noise server operation - Add startBackgroundServer method to NoisePaymentService - Create PushNotificationService for sending wake notifications - Create ReceiptService for managing receipt-request associations - Add invoiceNumber and receiptId fields to PaymentRequest model - Add requestId and invoiceNumber to Receipt model - Add cross-reference methods to PaymentRequestStorage - Add push notification endpoint publishing to DirectoryService - Update PaymentRequestsScreen UI to display invoice numbers and linked receipts
1 parent 2fc0534 commit 8dc6a78

25 files changed

+4444
-33
lines changed

RELEASE_NOTES.md

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
# Release Notes
2+
3+
## Paykit Integration Release
4+
5+
### Version 1.0.0 - Paykit Edition
6+
7+
**Release Date:** December 2025
8+
9+
---
10+
11+
## What's New
12+
13+
### 🎉 Paykit Integration
14+
15+
Bitkit now supports Paykit, enabling seamless payments with your Pubky identity:
16+
17+
- **Pay Anyone with Pubky**: Send Bitcoin to any Pubky username
18+
- **Automatic Discovery**: Payment methods are discovered automatically
19+
- **Secure Sessions**: Connect with Pubky-ring for full functionality
20+
- **Direct Payments**: Encrypted peer-to-peer payments via Noise Protocol
21+
22+
### Features
23+
24+
#### Session Management
25+
- Connect Bitkit to your Pubky-ring identity
26+
- Cross-device authentication via QR code
27+
- Automatic session refresh
28+
- Secure session storage in Android Keystore
29+
30+
#### Payment Discovery
31+
- Automatic lookup of payment methods
32+
- Support for Lightning, on-chain, and interactive payments
33+
- Fallback chains when primary method fails
34+
- Contact-based payments
35+
36+
#### Contacts
37+
- Sync contacts from your Pubky follows
38+
- Discover payment-enabled contacts
39+
- Quick payments to saved contacts
40+
41+
#### Backup & Restore
42+
- Export sessions and settings
43+
- Password-protected encryption
44+
- Cross-device migration support
45+
46+
---
47+
48+
## Improvements
49+
50+
- Enhanced error messages for payment failures
51+
- Faster Lightning payment execution
52+
- Improved network resilience
53+
- Better battery efficiency for background sync
54+
- Material3 UI updates for Paykit screens
55+
56+
---
57+
58+
## Bug Fixes
59+
60+
- Fixed session expiry not being detected in some cases
61+
- Fixed rare crash during QR code scanning
62+
- Fixed memory leak in contact list
63+
- Fixed incorrect amount display in some locales
64+
- Fixed state restoration after process death
65+
66+
---
67+
68+
## Technical Details
69+
70+
### New Dependencies
71+
- pubky-noise 1.0.0
72+
- paykit-lib 1.0.0
73+
- paykit-interactive 1.0.0
74+
75+
### Minimum Requirements
76+
- Android 9.0 (API 28)+
77+
- Active internet connection
78+
- Optional: Pubky-ring app for full functionality
79+
80+
### Known Limitations
81+
- Interactive payments require Pubky-ring
82+
- Some features limited without active session
83+
- Background sync requires sufficient battery
84+
85+
---
86+
87+
## Upgrade Notes
88+
89+
### From Previous Versions
90+
91+
1. Update Bitkit to latest version
92+
2. Wallet data migrates automatically
93+
3. Connect to Pubky-ring to enable Paykit features
94+
4. Sync contacts if desired
95+
96+
### New Users
97+
98+
1. Install Bitkit
99+
2. Create or restore wallet
100+
3. Install Pubky-ring (recommended)
101+
4. Connect Paykit in Settings
102+
103+
---
104+
105+
## Documentation
106+
107+
- [User Guide](docs/USER_GUIDE.md)
108+
- [Setup Guide](docs/PAYKIT_SETUP.md)
109+
- [Troubleshooting](docs/USER_GUIDE.md#troubleshooting)
110+
111+
---
112+
113+
## Feedback
114+
115+
We'd love to hear from you:
116+
- In-app: Settings → Help & Support
117+
- GitHub: Open an issue
118+
- Community: Join our Telegram/Discord
119+
120+
---
121+
122+
## Contributors
123+
124+
Thanks to everyone who contributed to this release!
125+
126+
---
127+
128+
## Full Changelog
129+
130+
For the complete list of changes, see [CHANGELOG.md](CHANGELOG.md).
131+

app/src/main/AndroidManifest.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@
66
<intent>
77
<action android:name="android.settings.APPLICATION_DETAILS_SETTINGS" />
88
</intent>
9+
<!-- Pubky Ring package visibility for Android 11+ -->
10+
<package android:name="to.pubky.ring" />
11+
<intent>
12+
<action android:name="android.intent.action.VIEW" />
13+
<data android:scheme="pubkyring" />
14+
</intent>
915
</queries>
1016

1117
<uses-feature

app/src/main/java/to/bitkit/data/SettingsStore.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,11 @@ data class SettingsData(
8383
val defaultTransactionSpeed: TransactionSpeed = TransactionSpeed.default(),
8484
val showEmptyBalanceView: Boolean = true,
8585
val hasSeenSpendingIntro: Boolean = false,
86+
// Profile data (cached locally)
87+
val profileName: String = "",
88+
val profileBio: String = "",
89+
val profileAvatarUrl: String = "",
90+
val profilePubkyId: String = "",
8691
val hasSeenWidgetsIntro: Boolean = false,
8792
val hasSeenTransferIntro: Boolean = false,
8893
val hasSeenSavingsIntro: Boolean = false,

app/src/main/java/to/bitkit/fcm/FcmService.kt

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import to.bitkit.env.Env.DERIVATION_NAME
1717
import to.bitkit.ext.fromBase64
1818
import to.bitkit.ext.fromHex
1919
import to.bitkit.models.BlocktankNotificationType
20+
import to.bitkit.models.BlocktankNotificationType.paykitNoiseRequest
21+
import to.bitkit.paykit.workers.NoiseServerWorker
2022
import to.bitkit.repositories.LightningRepo
2123
import to.bitkit.ui.pushNotification
2224
import to.bitkit.utils.Crypto
@@ -76,6 +78,12 @@ class FcmService : FirebaseMessagingService() {
7678
}
7779

7880
private fun handleAsync() {
81+
// Check if this is a Noise request - handle separately
82+
if (notificationType == paykitNoiseRequest) {
83+
handleNoiseRequest()
84+
return
85+
}
86+
7987
val work = OneTimeWorkRequestBuilder<WakeNodeWorker>()
8088
.setInputData(
8189
workDataOf(
@@ -88,6 +96,35 @@ class FcmService : FirebaseMessagingService() {
8896
.beginWith(work)
8997
.enqueue()
9098
}
99+
100+
/**
101+
* Handle incoming Noise protocol request by starting NoiseServerWorker
102+
*/
103+
private fun handleNoiseRequest() {
104+
Logger.debug("Handling incoming Noise request notification", context = TAG)
105+
106+
// Extract Noise-specific data from payload
107+
val fromPubkey = notificationPayload?.get("from_pubkey")?.toString()?.removeSurrounding("\"")
108+
val endpointHost = notificationPayload?.get("endpoint_host")?.toString()?.removeSurrounding("\"")
109+
val endpointPort = notificationPayload?.get("endpoint_port")?.toString()?.removeSurrounding("\"")?.toIntOrNull() ?: 9000
110+
val noisePubkey = notificationPayload?.get("noise_pubkey")?.toString()?.removeSurrounding("\"")
111+
112+
val work = OneTimeWorkRequestBuilder<NoiseServerWorker>()
113+
.setInputData(
114+
workDataOf(
115+
NoiseServerWorker.KEY_FROM_PUBKEY to fromPubkey,
116+
NoiseServerWorker.KEY_ENDPOINT_HOST to endpointHost,
117+
NoiseServerWorker.KEY_ENDPOINT_PORT to endpointPort,
118+
NoiseServerWorker.KEY_NOISE_PUBKEY to noisePubkey,
119+
)
120+
)
121+
.build()
122+
123+
WorkManager.getInstance(this)
124+
.enqueue(work)
125+
126+
Logger.info("Scheduled NoiseServerWorker for incoming request from ${fromPubkey?.take(12)}...", context = TAG)
127+
}
91128

92129
private fun handleNow(data: Map<String, String>) {
93130
Logger.warn("FCM handler not implemented for: $data")

app/src/main/java/to/bitkit/models/BlocktankNotificationType.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@ enum class BlocktankNotificationType {
1515
paykitPaymentRequest,
1616
paykitSubscriptionDue,
1717
paykitAutoPayExecuted,
18-
paykitSubscriptionFailed;
18+
paykitSubscriptionFailed,
19+
20+
/** Incoming Noise protocol request - wake app to start Noise server */
21+
paykitNoiseRequest;
1922

2023
override fun toString(): String = when {
2124
name.startsWith("paykit") -> "paykit.$name"

app/src/main/java/to/bitkit/paykit/models/PaymentRequest.kt

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,11 @@ data class PaymentRequest(
2727
val createdAt: Long = System.currentTimeMillis(),
2828
val expiresAt: Long? = null,
2929
var status: PaymentRequestStatus = PaymentRequestStatus.PENDING,
30-
val direction: RequestDirection
30+
val direction: RequestDirection,
31+
/** Optional invoice number for cross-referencing with receipts */
32+
val invoiceNumber: String? = null,
33+
/** ID of the receipt that fulfilled this request (if paid) */
34+
var receiptId: String? = null
3135
) {
3236
val counterpartyName: String
3337
get() {
@@ -38,4 +42,12 @@ data class PaymentRequest(
3842
key
3943
}
4044
}
45+
46+
/** Display invoice number - returns invoiceNumber if set, otherwise request id */
47+
val displayInvoiceNumber: String
48+
get() = invoiceNumber ?: id
49+
50+
/** Check if this request has been fulfilled */
51+
val isFulfilled: Boolean
52+
get() = status == PaymentRequestStatus.PAID && receiptId != null
4153
}

app/src/main/java/to/bitkit/paykit/models/Receipt.kt

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,11 @@ data class Receipt(
2828
var txId: String? = null,
2929
var proof: String? = null,
3030
var proofVerified: Boolean = false,
31-
var proofVerifiedAt: Long? = null
31+
var proofVerifiedAt: Long? = null,
32+
/** ID of the payment request this receipt fulfills (if any) */
33+
val requestId: String? = null,
34+
/** Invoice number from the original request (for cross-referencing) */
35+
val invoiceNumber: String? = null
3236
) {
3337
companion object {
3438
fun create(
@@ -37,7 +41,9 @@ data class Receipt(
3741
counterpartyName: String? = null,
3842
amountSats: Long,
3943
paymentMethod: String,
40-
memo: String? = null
44+
memo: String? = null,
45+
requestId: String? = null,
46+
invoiceNumber: String? = null
4147
): Receipt {
4248
return Receipt(
4349
id = UUID.randomUUID().toString(),
@@ -46,7 +52,9 @@ data class Receipt(
4652
counterpartyName = counterpartyName,
4753
amountSats = amountSats,
4854
paymentMethod = paymentMethod,
49-
memo = memo
55+
memo = memo,
56+
requestId = requestId,
57+
invoiceNumber = invoiceNumber
5058
)
5159
}
5260
}

app/src/main/java/to/bitkit/paykit/services/DirectoryService.kt

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,88 @@ class DirectoryService @Inject constructor(
161161
}
162162
}
163163

164+
// MARK: - Push Notification Endpoints
165+
166+
/**
167+
* Push endpoint for receiving wake notifications
168+
*/
169+
@kotlinx.serialization.Serializable
170+
data class PushNotificationEndpoint(
171+
val deviceToken: String,
172+
val platform: String, // "ios" or "android"
173+
val noiseHost: String? = null,
174+
val noisePort: Int? = null,
175+
val noisePubkey: String? = null,
176+
val createdAt: Long = System.currentTimeMillis() / 1000
177+
)
178+
179+
/**
180+
* Publish our push notification endpoint to the directory.
181+
* This allows other users to discover how to wake our device for Noise connections.
182+
*/
183+
suspend fun publishPushNotificationEndpoint(
184+
deviceToken: String,
185+
platform: String,
186+
noiseHost: String? = null,
187+
noisePort: Int? = null,
188+
noisePubkey: String? = null
189+
) {
190+
val transport = authenticatedTransport ?: throw DirectoryError.NotConfigured
191+
192+
val endpoint = PushNotificationEndpoint(
193+
deviceToken = deviceToken,
194+
platform = platform,
195+
noiseHost = noiseHost,
196+
noisePort = noisePort,
197+
noisePubkey = noisePubkey
198+
)
199+
200+
val pushPath = "${PAYKIT_PATH_PREFIX}push"
201+
val json = kotlinx.serialization.json.Json.encodeToString(endpoint)
202+
203+
try {
204+
pubkyStorage.store(pushPath, json.toByteArray())
205+
Logger.info("Published push notification endpoint to directory", context = TAG)
206+
} catch (e: Exception) {
207+
Logger.error("Failed to publish push endpoint", e, context = TAG)
208+
throw DirectoryError.PublishFailed(e.message ?: "Unknown error")
209+
}
210+
}
211+
212+
/**
213+
* Discover push notification endpoint for a recipient.
214+
* Used to send wake notifications before attempting Noise connections.
215+
*/
216+
suspend fun discoverPushNotificationEndpoint(recipientPubkey: String): PushNotificationEndpoint? {
217+
val adapter = PubkyUnauthenticatedStorageAdapter(homeserverBaseURL)
218+
val pushPath = "${PAYKIT_PATH_PREFIX}push"
219+
220+
return try {
221+
val data = pubkyStorage.retrieve(pushPath, adapter, recipientPubkey) ?: return null
222+
val json = String(data)
223+
kotlinx.serialization.json.Json.decodeFromString<PushNotificationEndpoint>(json)
224+
} catch (e: Exception) {
225+
Logger.error("Failed to discover push endpoint for $recipientPubkey", e, context = TAG)
226+
null
227+
}
228+
}
229+
230+
/**
231+
* Remove our push notification endpoint from the directory.
232+
*/
233+
suspend fun removePushNotificationEndpoint() {
234+
val transport = authenticatedTransport ?: throw DirectoryError.NotConfigured
235+
val pushPath = "${PAYKIT_PATH_PREFIX}push"
236+
237+
try {
238+
pubkyStorage.delete(pushPath)
239+
Logger.info("Removed push notification endpoint from directory", context = TAG)
240+
} catch (e: Exception) {
241+
Logger.error("Failed to remove push endpoint", e, context = TAG)
242+
throw DirectoryError.PublishFailed(e.message ?: "Unknown error")
243+
}
244+
}
245+
164246
/**
165247
* Discover payment methods for a recipient
166248
*/

0 commit comments

Comments
 (0)