diff --git a/.gitignore b/.gitignore
index 802156a..c7c9160 100644
--- a/.gitignore
+++ b/.gitignore
@@ -380,3 +380,6 @@ vendor/bundle/
# PR documentation (local only, not for git)
PR_SUMMARY.md
PR_README.md
+
+# third_party
+third_party/
\ No newline at end of file
diff --git a/App.tsx b/App.tsx
index 2df1b0c..ccdc159 100644
--- a/App.tsx
+++ b/App.tsx
@@ -23,6 +23,8 @@ import {
NativeEventEmitter,
Platform,
DeviceEventEmitter,
+ View,
+ StyleSheet,
} from 'react-native';
import WalletSettings from './screens/WalletSettings';
import {NativeModules} from 'react-native';
@@ -313,58 +315,67 @@ const App = () => {
-
-
- null,
- }}
- />
- null,
- }}
- />
-
-
-
-
-
-
+
+
+
+ null,
+ contentStyle: {backgroundColor: '#ffffff'},
+ }}
+ />
+ null,
+ contentStyle: {backgroundColor: '#ffffff'},
+ }}
+ />
+
+
+
+
+
+
+
@@ -373,4 +384,11 @@ const App = () => {
);
};
+const styles = StyleSheet.create({
+ navigationContainer: {
+ flex: 1,
+ backgroundColor: '#ffffff',
+ },
+});
+
export default App;
diff --git a/BBMTLib/build-bold-spend.sh b/BBMTLib/bold-spend-builder.sh
similarity index 100%
rename from BBMTLib/build-bold-spend.sh
rename to BBMTLib/bold-spend-builder.sh
diff --git a/BBMTLib/tss/btc.go b/BBMTLib/tss/btc.go
index 49bbc35..a6ad3ec 100644
--- a/BBMTLib/tss/btc.go
+++ b/BBMTLib/tss/btc.go
@@ -27,9 +27,13 @@ import (
// UTXO represents an unspent transaction output
type UTXO struct {
- TxID string `json:"txid"`
- Vout uint32 `json:"vout"`
- Value int64 `json:"value"` // Value in satoshis
+ TxID string `json:"txid"`
+ Vout uint32 `json:"vout"`
+ Value int64 `json:"value"` // Value in satoshis
+ Status struct {
+ Confirmed bool `json:"confirmed"`
+ BlockHeight int64 `json:"block_height"`
+ } `json:"status,omitempty"` // Status is optional, includes both confirmed and unconfirmed UTXOs
}
var _btc_net = "testnet3" // default to testnet
@@ -79,18 +83,76 @@ func GetNetwork() (string, error) {
}
// FetchUTXOs fetches UTXOs for a given address
+// The mempool.space API returns both confirmed and unconfirmed UTXOs by default
func FetchUTXOs(address string) ([]UTXO, error) {
url := fmt.Sprintf("%s/address/%s/utxo", _api_url, address)
+ Logf("Fetching UTXOs from endpoint: %s", url)
resp, err := http.Get(url)
if err != nil {
+ Logf("Error fetching UTXOs from %s: %v", url, err)
return nil, fmt.Errorf("failed to fetch UTXOs: %w", err)
}
defer resp.Body.Close()
+ // Check response status
+ if resp.StatusCode != http.StatusOK {
+ body, _ := io.ReadAll(resp.Body)
+ Logf("Error response from %s: HTTP %d - %s", url, resp.StatusCode, string(body))
+ return nil, fmt.Errorf("failed to fetch UTXOs: HTTP %d - %s", resp.StatusCode, string(body))
+ }
+ Logf("Successfully fetched UTXOs from %s (HTTP %d)", url, resp.StatusCode)
+
+ // Read the response body to log it
+ bodyBytes, err := io.ReadAll(resp.Body)
+ if err != nil {
+ Logf("Error reading response body from %s: %v", url, err)
+ return nil, fmt.Errorf("failed to read UTXO response: %w", err)
+ }
+
+ // Log the raw response (first 500 chars to avoid huge logs)
+ bodyStr := string(bodyBytes)
+ if len(bodyStr) > 500 {
+ Logf("Raw API response (first 500 chars): %s...", bodyStr[:500])
+ } else {
+ Logf("Raw API response: %s", bodyStr)
+ }
+
+ // Decode JSON from the body bytes
var utxos []UTXO
- if err := json.NewDecoder(resp.Body).Decode(&utxos); err != nil {
+ if err := json.Unmarshal(bodyBytes, &utxos); err != nil {
+ Logf("Error parsing JSON response from %s: %v. Response was: %s", url, err, bodyStr)
return nil, fmt.Errorf("failed to parse UTXO response: %w", err)
}
+
+ // Log UTXO status for debugging
+ if len(utxos) > 0 {
+ confirmedCount := 0
+ unconfirmedCount := 0
+ totalValue := int64(0)
+ for _, utxo := range utxos {
+ totalValue += utxo.Value
+ if utxo.Status.Confirmed {
+ confirmedCount++
+ } else {
+ unconfirmedCount++
+ }
+ }
+ Logf("Fetched %d UTXOs: %d confirmed, %d unconfirmed, total value: %d satoshis", len(utxos), confirmedCount, unconfirmedCount, totalValue)
+ // Log first few UTXOs for debugging
+ for i, utxo := range utxos {
+ if i < 3 { // Log first 3 UTXOs
+ Logf("UTXO[%d]: txid=%s, vout=%d, value=%d, confirmed=%v", i, utxo.TxID, utxo.Vout, utxo.Value, utxo.Status.Confirmed)
+ }
+ }
+ } else {
+ Logf("No UTXOs found for address %s (this includes both confirmed and unconfirmed)", address)
+ Logf("API returned empty array. This could mean:")
+ Logf(" - Address has no UTXOs (all spent)")
+ Logf(" - Address format mismatch")
+ Logf(" - Network mismatch (checking testnet vs mainnet)")
+ Logf(" - API endpoint issue")
+ }
+
return utxos, nil
}
@@ -119,8 +181,10 @@ func TotalUTXO(address string) (result string, err error) {
func FetchUTXODetails(txID string, vout uint32) (*wire.TxOut, bool, error) {
url := fmt.Sprintf("%s/tx/%s", _api_url, txID)
+ Logf("Fetching UTXO details from endpoint: %s", url)
resp, err := http.Get(url)
if err != nil {
+ Logf("Error fetching UTXO details from %s: %v", url, err)
return nil, false, fmt.Errorf("failed to fetch transaction details: %w", err)
}
defer resp.Body.Close()
@@ -151,8 +215,10 @@ func RecommendedFees(feeType string) (int, error) {
for _, url := range _api_urls {
fee_url := strings.TrimSuffix(url, "/")
url := fmt.Sprintf("%s/v1/fees/recommended", fee_url)
+ Logf("Fetching recommended fees from endpoint: %s (fee type: %s)", url, feeType)
resp, err := http.Get(url)
if err != nil {
+ Logf("Error fetching fees from %s: %v, trying next endpoint", url, err)
continue
}
defer resp.Body.Close()
@@ -200,10 +266,12 @@ func PostTx(rawTxHex string) (string, error) {
func postTxOnce(rawTxHex string) (string, error) {
// Define the Blockstream API endpoint for broadcasting transactions
url := fmt.Sprintf("%s/tx", _api_url)
+ Logf("Broadcasting transaction to endpoint: %s", url)
// Create a POST request with the raw transaction hex as the body
req, err := http.NewRequest("POST", url, bytes.NewBufferString(rawTxHex))
if err != nil {
+ Logf("Error creating POST request to %s: %v", url, err)
return "", fmt.Errorf("failed to create request: %w", err)
}
@@ -350,23 +418,55 @@ func EstimateFees(senderAddress, receiverAddress string, amountSatoshi int64) (r
}
}()
- Logln("BBMTLog", "invoking SendBitcoin...")
+ Logln("BBMTLog", "invoking EstimateFees...")
utxos, err := FetchUTXOs(senderAddress)
if err != nil {
return "", fmt.Errorf("failed to fetch UTXOs: %w", err)
}
- // select the utxos
+ // Check if we have any UTXOs
+ if len(utxos) == 0 {
+ Logf("No UTXOs found for address %s during fee estimation", senderAddress)
+ return "", fmt.Errorf("no UTXOs available for address %s. Please ensure you have confirmed transactions before sending", senderAddress)
+ }
+
+ // Use iterative approach to match MpcSendBTC behavior:
+ // MpcSendBTC selects UTXOs for amountSatoshi+estimatedFee, so we need to do the same
+ // 1. First estimate: select UTXOs for amount only, calculate fee
+ // 2. Second estimate: re-select UTXOs for amount+fee (matching actual send), re-calculate fee
+ // This ensures we select the same UTXOs that will be used in the actual send
+
+ // First iteration: select UTXOs for amount only
selectedUTXOs, _, err := SelectUTXOs(utxos, amountSatoshi, "smallest")
if err != nil {
return "", err
}
+ // First fee estimate with UTXOs selected for amount only
_fee, _err := calculateFees(senderAddress, selectedUTXOs, amountSatoshi, receiverAddress)
if _err != nil {
return "", _err
}
+
+ // Second iteration: re-select UTXOs for amount + fee (matching MpcSendBTC behavior)
+ // This ensures the fee estimation uses the same UTXOs that will be used in actual send
+ Logf("Re-selecting UTXOs for amount+fee (%d + %d = %d) to match MpcSendBTC behavior", amountSatoshi, _fee, amountSatoshi+_fee)
+ selectedUTXOs, _, err = SelectUTXOs(utxos, amountSatoshi+_fee, "smallest")
+ if err != nil {
+ // If we can't select enough UTXOs for amount+fee, return the original fee estimate
+ // This can happen if the wallet doesn't have enough funds
+ Logf("Could not select UTXOs for amount+fee, using original estimate: %v", err)
+ return strconv.FormatInt(_fee, 10), nil
+ }
+
+ // Re-calculate fee with the new UTXOs (which match what will be used in actual send)
+ _fee, _err = calculateFees(senderAddress, selectedUTXOs, amountSatoshi, receiverAddress)
+ if _err != nil {
+ return "", _err
+ }
+ Logf("Final fee estimate with UTXOs selected for amount+fee: %d", _fee)
+
return strconv.FormatInt(_fee, 10), nil
}
@@ -647,11 +747,25 @@ func MpcSendBTC(
}
Logf("Fetched UTXOs: %+v", utxos)
+ // Check if we have any UTXOs
+ if len(utxos) == 0 {
+ Logf("No UTXOs found for address %s. This may be because:", senderAddress)
+ Logf("1. The address has no confirmed transactions")
+ Logf("2. All transactions are still pending (unconfirmed)")
+ Logf("3. The address has been fully spent")
+ return "", fmt.Errorf("no UTXOs available for address %s. Please ensure you have confirmed transactions before sending", senderAddress)
+ }
+
mpcHook("selecting utxos", session, "", 0, 0, false)
selectedUTXOs, totalAmount, err := SelectUTXOs(utxos, amountSatoshi+estimatedFee, "smallest")
if err != nil {
Logf("Error selecting UTXOs: %v", err)
- return "", err
+ // Provide more context in the error message
+ totalAvailable := int64(0)
+ for _, utxo := range utxos {
+ totalAvailable += utxo.Value
+ }
+ return "", fmt.Errorf("insufficient funds: needed %d (amount: %d + fee: %d), available: %d. Note: Only confirmed UTXOs are available for spending", amountSatoshi+estimatedFee, amountSatoshi, estimatedFee, totalAvailable)
}
Logf("Selected UTXOs: %+v, Total Amount: %d", selectedUTXOs, totalAmount)
@@ -1440,6 +1554,7 @@ func ReplaceTransaction(
// Fetch the original transaction details
url := fmt.Sprintf("%s/tx/%s", _api_url, originalTxID)
+ Logf("Fetching transaction details from endpoint: %s", url)
resp, err := http.Get(url)
if err != nil {
return "", fmt.Errorf("failed to fetch original transaction: %w", err)
diff --git a/BBMTLib/tss/mpc_nostr.go b/BBMTLib/tss/mpc_nostr.go
index a4d9e0c..07b126a 100644
--- a/BBMTLib/tss/mpc_nostr.go
+++ b/BBMTLib/tss/mpc_nostr.go
@@ -436,11 +436,9 @@ func runNostrPreAgreementSendBTC(relaysCSV, partyNsec, partiesNpubsCSV, sessionF
Logf("runNostrPreAgreementSendBTC: sending message: %s", localMessage)
// Context for the pre-agreement phase
- // Timeout: 2 minutes (120 seconds) to allow for:
- // - Network delays
- // - Retroactive message processing (messages sent before we started listening)
- // - Relay synchronization delays
- ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
+ // Timeout: 16 seconds - fail fast if peer doesn't respond quickly
+ // With resilient relays, messages should arrive quickly if peer is online
+ ctx, cancel := context.WithTimeout(context.Background(), 16*time.Second)
defer cancel()
// Channel to receive peer's message
@@ -483,8 +481,10 @@ func runNostrPreAgreementSendBTC(relaysCSV, partyNsec, partiesNpubsCSV, sessionF
}
}()
- // Small delay to ensure subscription is active before sending
- time.Sleep(1 * time.Second)
+ // Wait for subscription to be ready before sending
+ // Give the MessagePump time to establish subscriptions to all relays
+ // The subscription needs to be active to receive the peer's response
+ time.Sleep(2 * time.Second)
// Send our message to peer
err = messenger.SendMessage(ctx, localNpub, peerNpub, localMessage)
diff --git a/BBMTLib/tss/nostrtransport/client.go b/BBMTLib/tss/nostrtransport/client.go
index a685850..5b4c0f7 100644
--- a/BBMTLib/tss/nostrtransport/client.go
+++ b/BBMTLib/tss/nostrtransport/client.go
@@ -6,6 +6,7 @@ import (
"fmt"
"os"
"strings"
+ "sync"
"time"
nostr "github.com/nbd-wtf/go-nostr"
@@ -284,8 +285,16 @@ func (c *Client) Publish(ctx context.Context, event *Event) error {
fmt.Fprintf(os.Stderr, "BBMTLog: Client.Publish - signed event, PubKey (hex)=%s, tags=%v\n", event.PubKey, event.Tags)
- results := c.pool.PublishMany(ctx, c.urls, *event)
- totalRelays := len(c.urls)
+ // Use all valid relays, not just initially connected ones
+ // The pool will handle connections - if a relay isn't connected yet, it will try to connect
+ // This ensures we publish to all relays, including those that connected in background
+ relaysToUse := c.validRelays
+ if len(relaysToUse) == 0 {
+ // Fallback to urls if validRelays not set (backward compatibility)
+ relaysToUse = c.urls
+ }
+ results := c.pool.PublishMany(ctx, relaysToUse, *event)
+ totalRelays := len(relaysToUse)
// Track results in background goroutine - return immediately on first success
successCh := make(chan bool, 1)
@@ -385,42 +394,64 @@ func (c *Client) Publish(ctx context.Context, event *Event) error {
}
func (c *Client) Subscribe(ctx context.Context, filter Filter) (<-chan *Event, error) {
- if len(c.urls) == 0 {
- return nil, errors.New("no relays configured")
- }
- events := make(chan *Event)
-
// Use all valid relays, not just initially connected ones
- // The pool will handle connections - if a relay isn't connected yet, it will try to connect
- // This ensures we subscribe to all relays, including those that connected in background
relaysToUse := c.validRelays
if len(relaysToUse) == 0 {
// Fallback to urls if validRelays not set (backward compatibility)
relaysToUse = c.urls
}
- relayCh := c.pool.SubscribeMany(ctx, relaysToUse, filter)
+ if len(relaysToUse) == 0 {
+ return nil, errors.New("no relays configured")
+ }
+
+ events := make(chan *Event, 100) // Buffered to prevent blocking
+ totalRelays := len(relaysToUse)
// Track relay connection status
connectedRelays := make(map[string]bool)
- totalRelays := len(relaysToUse)
- var connectionCheckDone bool
+ connectedRelaysMu := sync.Mutex{}
+ firstConnectionCh := make(chan bool, 1) // Signal when first connection succeeds
+ connectionCheckDone := false
+
+ // Ensure at least one relay is connected before subscribing (in parallel, non-blocking)
+ // This ensures we have working connections before attempting subscription
+ // Connect to all relays in parallel, wait for at least one success
+ connectDone := make(chan string, totalRelays) // Relay URL when connected
+ connectTimeout := time.NewTimer(3 * time.Second)
+ defer connectTimeout.Stop()
+
+ // Try connecting to all relays in parallel
+ for _, url := range relaysToUse {
+ go func(relayURL string) {
+ relay, err := c.pool.EnsureRelay(relayURL)
+ if err == nil && relay != nil {
+ fmt.Fprintf(os.Stderr, "BBMTLog: Client.Subscribe - relay %s connected for subscription\n", relayURL)
+ select {
+ case connectDone <- relayURL:
+ default:
+ }
+ }
+ }(url)
+ }
- // Start a goroutine to monitor connection status
- connectionCtx, connectionCancel := context.WithTimeout(ctx, 5*time.Second)
- defer connectionCancel()
+ // Wait for at least one connection or timeout
+ connectedRelay := ""
+ select {
+ case connectedRelay = <-connectDone:
+ fmt.Fprintf(os.Stderr, "BBMTLog: Client.Subscribe - at least one relay connected (%s), proceeding with subscription\n", connectedRelay)
+ // Continue connecting others in background (non-blocking)
+ case <-connectTimeout.C:
+ // Timeout - proceed anyway, pool will handle connections
+ fmt.Fprintf(os.Stderr, "BBMTLog: Client.Subscribe - connection timeout, proceeding (pool will handle connections)\n")
+ case <-ctx.Done():
+ return nil, ctx.Err()
+ }
- go func() {
- <-connectionCtx.Done()
- if !connectionCheckDone {
- connectionCheckDone = true
- if len(connectedRelays) == 0 {
- fmt.Fprintf(os.Stderr, "BBMTLog: Client.Subscribe - WARNING: No relays connected after 5 seconds (all %d relays may have failed)\n", totalRelays)
- } else if len(connectedRelays) < totalRelays {
- fmt.Fprintf(os.Stderr, "BBMTLog: Client.Subscribe - %d/%d relays connected\n", len(connectedRelays), totalRelays)
- }
- }
- }()
+ // Start subscription to all relays in parallel (non-blocking)
+ // SubscribeMany connects to all relays in parallel and merges their events
+ relayCh := c.pool.SubscribeMany(ctx, relaysToUse, filter)
+ // Process events and track connections in background
go func() {
defer close(events)
for {
@@ -430,47 +461,74 @@ func (c *Client) Subscribe(ctx context.Context, filter Filter) (<-chan *Event, e
case relayEvent, ok := <-relayCh:
if !ok {
// Channel closed - check if we ever got any connections
+ connectedRelaysMu.Lock()
connectionCheckDone = true
if len(connectedRelays) == 0 {
fmt.Fprintf(os.Stderr, "BBMTLog: Client.Subscribe - ERROR: All %d relays failed to connect or disconnected\n", totalRelays)
} else {
fmt.Fprintf(os.Stderr, "BBMTLog: Client.Subscribe - subscription closed (%d/%d relays were connected)\n", len(connectedRelays), totalRelays)
}
+ connectedRelaysMu.Unlock()
return
}
+
// Get relay URL for tracking
var relayURL string
if relayEvent.Relay != nil {
relayURL = relayEvent.Relay.URL
}
- if relayEvent.Event == nil {
- // Track relay connection (even if no event yet, the relay is responding)
- if relayURL != "" {
- if !connectedRelays[relayURL] {
- connectedRelays[relayURL] = true
- fmt.Fprintf(os.Stderr, "BBMTLog: Client.Subscribe - relay %s connected (%d/%d)\n", relayURL, len(connectedRelays), totalRelays)
- }
- }
- continue
- }
- // Track relay connection when we receive an event
+ // Track connection when we receive events (relay is working)
if relayURL != "" {
+ connectedRelaysMu.Lock()
if !connectedRelays[relayURL] {
connectedRelays[relayURL] = true
- fmt.Fprintf(os.Stderr, "BBMTLog: Client.Subscribe - relay %s connected (%d/%d)\n", relayURL, len(connectedRelays), totalRelays)
+ isFirst := len(connectedRelays) == 1
+ fmt.Fprintf(os.Stderr, "BBMTLog: Client.Subscribe - relay %s active (%d/%d) - received event\n", relayURL, len(connectedRelays), totalRelays)
+
+ // Signal first connection success (non-blocking)
+ if isFirst && !connectionCheckDone {
+ connectionCheckDone = true
+ select {
+ case firstConnectionCh <- true:
+ default:
+ }
+ }
}
+ connectedRelaysMu.Unlock()
}
- select {
- case events <- relayEvent.Event:
- case <-ctx.Done():
- return
+
+ // Forward events to the output channel
+ if relayEvent.Event != nil {
+ fmt.Fprintf(os.Stderr, "BBMTLog: Client.Subscribe - forwarding event from relay %s: kind=%d, pubkey=%s, content_len=%d, tags_count=%d\n", relayURL, relayEvent.Event.Kind, relayEvent.Event.PubKey, len(relayEvent.Event.Content), len(relayEvent.Event.Tags))
+ select {
+ case events <- relayEvent.Event:
+ fmt.Fprintf(os.Stderr, "BBMTLog: Client.Subscribe - event forwarded to subscription channel\n")
+ case <-ctx.Done():
+ return
+ }
+ } else {
+ fmt.Fprintf(os.Stderr, "BBMTLog: Client.Subscribe - relayEvent.Event is nil from relay %s\n", relayURL)
}
}
}
}()
- return events, nil
+ // Wait for subscription to be ready - give it time to establish
+ // We've already ensured at least one relay is connected, so subscription should work
+ select {
+ case <-firstConnectionCh:
+ // Subscription ready - at least one relay is receiving events
+ fmt.Fprintf(os.Stderr, "BBMTLog: Client.Subscribe - subscription ready, active\n")
+ return events, nil
+ case <-ctx.Done():
+ return nil, ctx.Err()
+ case <-time.After(1 * time.Second):
+ // After 1 second, assume subscription is established
+ // We've already connected to at least one relay, so subscription should work
+ fmt.Fprintf(os.Stderr, "BBMTLog: Client.Subscribe - subscription established (at least one relay connected)\n")
+ return events, nil
+ }
}
// PublishWrap publishes a pre-signed gift wrap event (kind:1059)
@@ -495,8 +553,16 @@ func (c *Client) PublishWrap(ctx context.Context, wrap *Event) error {
wrap.CreatedAt = nostr.Now()
}
- results := c.pool.PublishMany(ctx, c.urls, *wrap)
- totalRelays := len(c.urls)
+ // Use all valid relays, not just initially connected ones
+ // The pool will handle connections - if a relay isn't connected yet, it will try to connect
+ // This ensures we publish to all relays, including those that connected in background
+ relaysToUse := c.validRelays
+ if len(relaysToUse) == 0 {
+ // Fallback to urls if validRelays not set (backward compatibility)
+ relaysToUse = c.urls
+ }
+ results := c.pool.PublishMany(ctx, relaysToUse, *wrap)
+ totalRelays := len(relaysToUse)
// Track results in background goroutine - return immediately on first success
successCh := make(chan bool, 1)
diff --git a/BBMTLib/tss/nostrtransport/pump.go b/BBMTLib/tss/nostrtransport/pump.go
index a984e22..1ed16d4 100644
--- a/BBMTLib/tss/nostrtransport/pump.go
+++ b/BBMTLib/tss/nostrtransport/pump.go
@@ -87,6 +87,210 @@ func (p *MessagePump) Run(ctx context.Context, handler func([]byte) error) error
retryTicker := time.NewTicker(1 * time.Second)
defer retryTicker.Stop()
+ // Helper function to process an event (unwrap, verify, and call handler)
+ processEvent := func(event *nostr.Event) error {
+ if event == nil {
+ return nil
+ }
+
+ fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump received event from %s (hex), kind=%d, content_len=%d, tags_count=%d\n", event.PubKey, event.Kind, len(event.Content), len(event.Tags))
+
+ // Verify it's a gift wrap (kind:1059)
+ if event.Kind != 1059 {
+ fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump skipping non-wrap event (kind=%d)\n", event.Kind)
+ return nil
+ }
+
+ // Step 1: Unwrap the gift wrap to get the seal
+ seal, err := unwrapGift(event, p.cfg.LocalNsec)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump failed to unwrap gift: %v\n", err)
+ return nil
+ }
+ fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump unwrapped gift, got seal from %s\n", seal.PubKey)
+
+ // Verify seal is from an expected peer
+ sealSenderNpub := seal.PubKey
+ isFromExpectedPeer := false
+ for _, expectedNpub := range p.cfg.PeersNpub {
+ expectedHex, err := npubToHex(expectedNpub)
+ if err != nil {
+ continue
+ }
+ if sealSenderNpub == expectedHex {
+ isFromExpectedPeer = true
+ break
+ }
+ }
+ if !isFromExpectedPeer {
+ fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump seal from unexpected sender (hex: %s)\n", sealSenderNpub)
+ return nil
+ }
+
+ // Step 2: Unseal to get the rumor
+ // Convert seal sender npub to bech32 format for unseal (it expects npub format)
+ sealSenderNpubBech32 := sealSenderNpub
+ for _, npub := range p.cfg.PeersNpub {
+ npubHex, err := npubToHex(npub)
+ if err == nil && npubHex == sealSenderNpub {
+ sealSenderNpubBech32 = npub
+ break
+ }
+ }
+
+ rumor, err := unseal(seal, p.cfg.LocalNsec, sealSenderNpubBech32)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump failed to unseal: %v\n", err)
+ return nil
+ }
+ fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump unsealed, got rumor\n")
+
+ // Step 3: Extract chunk data from rumor
+ var chunkMessage map[string]interface{}
+ if err := json.Unmarshal([]byte(rumor.Content), &chunkMessage); err != nil {
+ fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump failed to parse rumor content: %v\n", err)
+ return nil
+ }
+
+ sessionIDValue, ok := chunkMessage["session_id"].(string)
+ if !ok {
+ fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump rumor missing session_id\n")
+ return nil
+ }
+ if sessionIDValue != p.cfg.SessionID {
+ fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump session mismatch (got %s, expected %s)\n", sessionIDValue, p.cfg.SessionID)
+ return nil
+ }
+
+ // Check if this is a ready/complete message (handled by SessionCoordinator, not MessagePump)
+ if _, ok := chunkMessage["phase"].(string); ok {
+ // This is a ready/complete message, skip it (handled by SessionCoordinator)
+ return nil
+ }
+
+ // Extract chunk metadata
+ chunkTagValue, ok := chunkMessage["chunk"].(string)
+ if !ok {
+ fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump rumor missing chunk metadata\n")
+ return nil
+ }
+
+ meta, err := ParseChunkTag(chunkTagValue)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump failed to parse chunk tag '%s': %v\n", chunkTagValue, err)
+ return nil
+ }
+ meta.SessionID = p.cfg.SessionID
+ fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump parsed chunk metadata: hash=%s, index=%d/%d\n", meta.Hash, meta.Index, meta.Total)
+
+ // Extract chunk data
+ chunkDataB64, ok := chunkMessage["data"].(string)
+ if !ok {
+ fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump rumor missing chunk data\n")
+ return nil
+ }
+
+ chunkData, err := base64.StdEncoding.DecodeString(chunkDataB64)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump failed to decode chunk data: %v\n", err)
+ return nil
+ }
+ fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump decoded chunk data: %d bytes\n", len(chunkData))
+
+ // Check if already processed
+ p.processedMu.Lock()
+ if p.processed[meta.Hash] {
+ fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump message %s already processed, skipping\n", meta.Hash)
+ p.processedMu.Unlock()
+ return nil
+ }
+ p.processedMu.Unlock()
+
+ // Add chunk to assembler
+ fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump adding chunk %d/%d to assembler\n", meta.Index+1, meta.Total)
+ reassembled, complete := p.assembler.Add(meta, chunkData)
+ if !complete {
+ fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump chunk %d/%d added, waiting for more chunks\n", meta.Index+1, meta.Total)
+ return nil
+ }
+ fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump all chunks received, reassembled %d bytes\n", len(reassembled))
+
+ hashBytes := sha256.Sum256(reassembled)
+ calculatedHash := hex.EncodeToString(hashBytes[:])
+ if !strings.EqualFold(calculatedHash, meta.Hash) {
+ fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump chunk hash mismatch (calc=%s, expected=%s)\n", calculatedHash, meta.Hash)
+ return nil
+ }
+
+ // Reassemble the full message from chunks (chunks are plaintext now, not encrypted)
+ // The reassembled data is the full message body
+ plaintext := reassembled
+
+ // Mark as processed
+ p.processedMu.Lock()
+ p.processed[meta.Hash] = true
+ p.processedMu.Unlock()
+
+ // Call handler with plaintext payload
+ fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump calling handler with %d bytes\n", len(plaintext))
+ if err := handler(plaintext); err != nil {
+ fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump handler error: %v\n", err)
+ return fmt.Errorf("handler error: %w", err)
+ }
+ fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump handler completed successfully\n")
+ return nil
+ }
+
+ // First, query for existing events BEFORE starting subscription
+ // This ensures we catch events that were published before we started listening
+ // Query in parallel to all relays (resilient - if one fails, others continue)
+ fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump querying for existing events for session %s (from last 1 minute)\n", p.cfg.SessionID)
+ queryCtx, queryCancel := context.WithTimeout(ctx, 3*time.Second)
+ defer queryCancel()
+
+ // Use all valid relays, not just initially connected ones
+ relaysToQuery := p.client.validRelays
+ if len(relaysToQuery) == 0 {
+ relaysToQuery = p.client.urls
+ }
+
+ queryDone := make(chan bool, 1)
+ go func() {
+ defer func() { queryDone <- true }()
+ // Query all relays in parallel
+ for _, url := range relaysToQuery {
+ go func(relayURL string) {
+ relay, err := p.client.GetPool().EnsureRelay(relayURL)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump failed to ensure relay %s for query: %v\n", relayURL, err)
+ return
+ }
+ existingEvents, err := relay.QuerySync(queryCtx, filter)
+ if err == nil {
+ fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump query on relay %s returned %d events for session %s\n", relayURL, len(existingEvents), p.cfg.SessionID)
+ for _, event := range existingEvents {
+ if event != nil {
+ // Process the event (this will call handler if it's a valid message)
+ processEvent(event)
+ }
+ }
+ } else {
+ fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump query on relay %s failed (non-fatal): %v\n", relayURL, err)
+ }
+ }(url)
+ }
+ // Give queries time to complete
+ time.Sleep(2 * time.Second)
+ }()
+
+ // Wait for initial query to complete (with timeout) before starting subscription
+ select {
+ case <-queryDone:
+ fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump initial query completed\n")
+ case <-time.After(3 * time.Second):
+ fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump initial query timeout, proceeding with subscription\n")
+ }
+
// Retry loop: resubscribe when channel closes (e.g., network disconnection)
for {
// Check if context is cancelled before attempting subscription
@@ -132,155 +336,18 @@ func (p *MessagePump) Run(ctx context.Context, handler func([]byte) error) error
}
break
}
- if event == nil {
- continue
- }
-
- fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump received event from %s (hex), kind=%d, content_len=%d, tags_count=%d\n", event.PubKey, event.Kind, len(event.Content), len(event.Tags))
-
- // Verify it's a gift wrap (kind:1059)
- if event.Kind != 1059 {
- fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump skipping non-wrap event (kind=%d)\n", event.Kind)
- continue
- }
-
- // Step 1: Unwrap the gift wrap to get the seal
- seal, err := unwrapGift(event, p.cfg.LocalNsec)
- if err != nil {
- fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump failed to unwrap gift: %v\n", err)
- continue
- }
- fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump unwrapped gift, got seal from %s\n", seal.PubKey)
-
- // Verify seal is from an expected peer
- sealSenderNpub := seal.PubKey
- isFromExpectedPeer := false
- for _, expectedNpub := range p.cfg.PeersNpub {
- expectedHex, err := npubToHex(expectedNpub)
- if err != nil {
- continue
- }
- if sealSenderNpub == expectedHex {
- isFromExpectedPeer = true
- break
- }
- }
- if !isFromExpectedPeer {
- fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump seal from unexpected sender (hex: %s)\n", sealSenderNpub)
- continue
- }
-
- // Step 2: Unseal to get the rumor
- // Convert seal sender npub to bech32 format for unseal (it expects npub format)
- sealSenderNpubBech32 := sealSenderNpub
- for _, npub := range p.cfg.PeersNpub {
- npubHex, err := npubToHex(npub)
- if err == nil && npubHex == sealSenderNpub {
- sealSenderNpubBech32 = npub
- break
- }
- }
-
- rumor, err := unseal(seal, p.cfg.LocalNsec, sealSenderNpubBech32)
- if err != nil {
- fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump failed to unseal: %v\n", err)
- continue
- }
- fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump unsealed, got rumor\n")
-
- // Step 3: Extract chunk data from rumor
- var chunkMessage map[string]interface{}
- if err := json.Unmarshal([]byte(rumor.Content), &chunkMessage); err != nil {
- fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump failed to parse rumor content: %v\n", err)
- continue
- }
-
- sessionIDValue, ok := chunkMessage["session_id"].(string)
- if !ok {
- fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump rumor missing session_id\n")
- continue
- }
- if sessionIDValue != p.cfg.SessionID {
- fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump session mismatch (got %s, expected %s)\n", sessionIDValue, p.cfg.SessionID)
- continue
- }
-
- // Check if this is a ready/complete message (handled by SessionCoordinator, not MessagePump)
- if _, ok := chunkMessage["phase"].(string); ok {
- // This is a ready/complete message, skip it (handled by SessionCoordinator)
- continue
- }
-
- // Extract chunk metadata
- chunkTagValue, ok := chunkMessage["chunk"].(string)
- if !ok {
- fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump rumor missing chunk metadata\n")
- continue
- }
-
- meta, err := ParseChunkTag(chunkTagValue)
- if err != nil {
- fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump failed to parse chunk tag '%s': %v\n", chunkTagValue, err)
- continue
- }
- meta.SessionID = p.cfg.SessionID
- fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump parsed chunk metadata: hash=%s, index=%d/%d\n", meta.Hash, meta.Index, meta.Total)
-
- // Extract chunk data
- chunkDataB64, ok := chunkMessage["data"].(string)
- if !ok {
- fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump rumor missing chunk data\n")
- continue
- }
-
- chunkData, err := base64.StdEncoding.DecodeString(chunkDataB64)
- if err != nil {
- fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump failed to decode chunk data: %v\n", err)
- continue
- }
- fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump decoded chunk data: %d bytes\n", len(chunkData))
-
- // Check if already processed
- p.processedMu.Lock()
- if p.processed[meta.Hash] {
- fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump message %s already processed, skipping\n", meta.Hash)
- p.processedMu.Unlock()
- continue
- }
- p.processedMu.Unlock()
-
- // Add chunk to assembler
- fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump adding chunk %d/%d to assembler\n", meta.Index+1, meta.Total)
- reassembled, complete := p.assembler.Add(meta, chunkData)
- if !complete {
- fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump chunk %d/%d added, waiting for more chunks\n", meta.Index+1, meta.Total)
- continue
- }
- fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump all chunks received, reassembled %d bytes\n", len(reassembled))
-
- hashBytes := sha256.Sum256(reassembled)
- calculatedHash := hex.EncodeToString(hashBytes[:])
- if !strings.EqualFold(calculatedHash, meta.Hash) {
- fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump chunk hash mismatch (calc=%s, expected=%s)\n", calculatedHash, meta.Hash)
+ // Log that we received an event from the subscription channel
+ if event != nil {
+ fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump received event from subscription channel: kind=%d, pubkey=%s, content_len=%d\n", event.Kind, event.PubKey, len(event.Content))
+ } else {
+ fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump received nil event from subscription channel\n")
continue
}
-
- // Reassemble the full message from chunks (chunks are plaintext now, not encrypted)
- // The reassembled data is the full message body
- plaintext := reassembled
-
- // Mark as processed
- p.processedMu.Lock()
- p.processed[meta.Hash] = true
- p.processedMu.Unlock()
-
- // Call handler with plaintext payload
- fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump calling handler with %d bytes\n", len(plaintext))
- if err := handler(plaintext); err != nil {
- fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump handler error: %v\n", err)
- return fmt.Errorf("handler error: %w", err)
+ // Process event using the helper function
+ if err := processEvent(event); err != nil {
+ // Handler error - return to stop processing
+ return err
}
- fmt.Fprintf(os.Stderr, "BBMTLog: MessagePump handler completed successfully\n")
}
}
// If we break out of the inner loop, we'll retry subscribing in the outer loop
diff --git a/BBMTLib/tss/nostrtransport/session.go b/BBMTLib/tss/nostrtransport/session.go
index eb57727..2e4d672 100644
--- a/BBMTLib/tss/nostrtransport/session.go
+++ b/BBMTLib/tss/nostrtransport/session.go
@@ -130,10 +130,16 @@ func (s *SessionCoordinator) AwaitPeers(ctx context.Context) error {
defer queryCancel()
// Query all relays in parallel and wait for results
+ // Use all valid relays, not just initially connected ones
+ relaysToQuery := s.client.validRelays
+ if len(relaysToQuery) == 0 {
+ // Fallback to urls if validRelays not set (backward compatibility)
+ relaysToQuery = s.client.urls
+ }
queryDone := make(chan bool, 1)
go func() {
defer func() { queryDone <- true }()
- for _, url := range s.client.urls {
+ for _, url := range relaysToQuery {
relay, err := s.client.GetPool().EnsureRelay(url)
if err != nil {
fmt.Fprintf(os.Stderr, "BBMTLog: Failed to ensure relay %s: %v\n", url, err)
@@ -205,10 +211,36 @@ func (s *SessionCoordinator) AwaitPeers(ctx context.Context) error {
}
// Now start subscription to catch new events
- fmt.Fprintf(os.Stderr, "BBMTLog: Starting subscription for ready wraps for session %s\n", s.cfg.SessionID)
- eventsCh, err := s.client.Subscribe(ctx, filter)
- if err != nil {
- return fmt.Errorf("subscribe to ready wraps: %w", err)
+ // Retry subscription if it fails or channel closes (resilient to relay failures)
+ retryTicker := time.NewTicker(1 * time.Second)
+ defer retryTicker.Stop()
+
+ var eventsCh <-chan *Event
+ subscriptionActive := false
+
+ // Retry loop for subscription
+ for !subscriptionActive {
+ select {
+ case <-ctx.Done():
+ fmt.Fprintf(os.Stderr, "BBMTLog: AwaitPeers timed out during subscription (seen: %d/%d)\n", s.countSeen(&seen), len(expected))
+ return fmt.Errorf("waiting for peers timed out: %w", ctx.Err())
+ default:
+ }
+
+ fmt.Fprintf(os.Stderr, "BBMTLog: Starting subscription for ready wraps for session %s\n", s.cfg.SessionID)
+ var err error
+ eventsCh, err = s.client.Subscribe(ctx, filter)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "BBMTLog: Subscribe failed: %v, retrying in 1 second...\n", err)
+ select {
+ case <-ctx.Done():
+ return fmt.Errorf("waiting for peers timed out: %w", ctx.Err())
+ case <-retryTicker.C:
+ continue // Retry subscription
+ }
+ }
+ subscriptionActive = true
+ fmt.Fprintf(os.Stderr, "BBMTLog: Subscription active for session %s\n", s.cfg.SessionID)
}
ticker := time.NewTicker(5 * time.Second)
@@ -222,7 +254,37 @@ func (s *SessionCoordinator) AwaitPeers(ctx context.Context) error {
return fmt.Errorf("waiting for peers timed out: %w", ctx.Err())
case evt, ok := <-eventsCh:
if !ok {
- return fmt.Errorf("relay subscription closed")
+ // Channel closed (e.g., relay disconnection) - retry subscription
+ fmt.Fprintf(os.Stderr, "BBMTLog: Subscription channel closed, retrying subscription in 1 second...\n")
+ subscriptionActive = false
+ // Wait before retrying
+ select {
+ case <-ctx.Done():
+ return fmt.Errorf("waiting for peers timed out: %w", ctx.Err())
+ case <-retryTicker.C:
+ // Retry subscription
+ for !subscriptionActive {
+ select {
+ case <-ctx.Done():
+ return fmt.Errorf("waiting for peers timed out: %w", ctx.Err())
+ default:
+ }
+ var err error
+ eventsCh, err = s.client.Subscribe(ctx, filter)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "BBMTLog: Subscribe retry failed: %v, retrying in 1 second...\n", err)
+ select {
+ case <-ctx.Done():
+ return fmt.Errorf("waiting for peers timed out: %w", ctx.Err())
+ case <-retryTicker.C:
+ continue // Retry subscription
+ }
+ }
+ subscriptionActive = true
+ fmt.Fprintf(os.Stderr, "BBMTLog: Subscription re-established for session %s\n", s.cfg.SessionID)
+ }
+ continue // Continue processing events
+ }
}
if evt == nil || evt.Kind != 1059 {
continue
diff --git a/BBMTLib/tss/psbt.go b/BBMTLib/tss/psbt.go
index 8daed7d..ae43c9c 100644
--- a/BBMTLib/tss/psbt.go
+++ b/BBMTLib/tss/psbt.go
@@ -197,6 +197,10 @@ func MpcSignPSBT(
inputDerivePath = extractedPath
Logf("Input %d: Using derivation path from PSBT Bip32Derivation: %s", i, inputDerivePath)
+ // Get the pubkey from PSBT's Bip32Derivation (this is what the PSBT expects)
+ psbtPubKeyBytes := input.Bip32Derivation[0].PubKey
+ Logf("Input %d: PSBT Bip32Derivation pubkey: %x", i, psbtPubKeyBytes)
+
// Derive the public key from keyshare using GetDerivedPubKey with the path from PSBT
// Use the keyshare's pub_key and chaincode to derive the corresponding public key
Logf("Input %d: Deriving public key from keyshare - xpub: %s, chaincode: %s, path: %s", i, truncateHex(keyshareData.PubKey), truncateHex(keyshareData.ChainCodeHex), inputDerivePath)
@@ -210,7 +214,15 @@ func MpcSignPSBT(
}
Logf("Input %d: Derived public key (hex): %s, bytes: %x (length: %d)", i, truncateHex(derivedPubKeyHex), derivedPubKeyBytes, len(derivedPubKeyBytes))
- // Use the derived key for signing
+ // Check if this input belongs to us by comparing derived pubkey with PSBT's pubkey
+ if !bytes.Equal(derivedPubKeyBytes, psbtPubKeyBytes) {
+ Logf("Input %d: ⚠️ SKIPPING SIGNATURE - Pubkey mismatch: derived %x != PSBT %x (path: %s)", i, derivedPubKeyBytes[:min(8, len(derivedPubKeyBytes))], psbtPubKeyBytes[:min(8, len(psbtPubKeyBytes))], inputDerivePath)
+ Logf("Input %d: This input does not belong to our wallet, skipping signature", i)
+ continue // Skip signing this input, but continue with other inputs
+ }
+
+ Logf("Input %d: ✅ Pubkey matches - this input belongs to our wallet, will sign", i)
+ // Use the derived key for signing (which matches PSBT's key)
inputPubKeyBytes = derivedPubKeyBytes
// Create session for this input
@@ -246,17 +258,7 @@ func MpcSignPSBT(
Logf("Input %d: Derived pubkey hash: %x", i, pubKeyHash)
Logf("Input %d: Derived pubkey: %x", i, inputPubKeyBytes)
- // Check PSBT's Bip32Derivation key if available
- if len(input.Bip32Derivation) > 0 {
- psbtPubKeyBytes := input.Bip32Derivation[0].PubKey
- psbtPubKeyHash := btcutil.Hash160(psbtPubKeyBytes)
- Logf("Input %d: PSBT Bip32Derivation pubkey: %x", i, psbtPubKeyBytes)
- Logf("Input %d: PSBT Bip32Derivation pubkey hash: %x", i, psbtPubKeyHash)
- if bytes.Equal(scriptPubKeyHash, psbtPubKeyHash) {
- Logf("Input %d: PSBT key matches script, but derived key doesn't - keyshare mismatch?", i)
- }
- }
-
+ // Verify the public key hash matches the script (we already verified pubkey ownership above)
if !bytes.Equal(scriptPubKeyHash, pubKeyHash) {
return "", fmt.Errorf("public key hash mismatch for input %d: script expects %x but got %x (derived from path: %s)", i, scriptPubKeyHash, pubKeyHash, inputDerivePath)
}
@@ -321,37 +323,7 @@ func MpcSignPSBT(
Logf("Input %d: Derived pubkey hash: %x", i, pubKeyHash)
Logf("Input %d: Derived pubkey: %x", i, inputPubKeyBytes)
- // Check PSBT's Bip32Derivation key if available
- if len(input.Bip32Derivation) > 0 {
- psbtPubKeyBytes := input.Bip32Derivation[0].PubKey
- psbtPubKeyHash := btcutil.Hash160(psbtPubKeyBytes)
- Logf("Input %d: PSBT Bip32Derivation pubkey: %x", i, psbtPubKeyBytes)
- Logf("Input %d: PSBT Bip32Derivation pubkey hash: %x", i, psbtPubKeyHash)
-
- // If derived key doesn't match script, but PSBT key does, verify we can derive PSBT key
- if !bytes.Equal(scriptPubKeyHash, pubKeyHash) && bytes.Equal(scriptPubKeyHash, psbtPubKeyHash) {
- Logf("Input %d: PSBT key matches script, but derived key doesn't - verifying if PSBT key can be derived from keyshare", i)
- // Try to derive the PSBT key from our keyshare
- verifyPubKeyHex, err := GetDerivedPubKey(keyshareData.PubKey, keyshareData.ChainCodeHex, inputDerivePath, false)
- if err != nil {
- return "", fmt.Errorf("input %d: failed to verify PSBT key derivation: %w", i, err)
- }
- verifyPubKeyBytes, err := hex.DecodeString(verifyPubKeyHex)
- if err != nil {
- return "", fmt.Errorf("input %d: failed to decode verified public key: %w", i, err)
- }
- if bytes.Equal(verifyPubKeyBytes, psbtPubKeyBytes) {
- // We can derive it - use the PSBT key
- Logf("Input %d: Verified PSBT key can be derived from keyshare - using PSBT key", i)
- inputPubKeyBytes = psbtPubKeyBytes
- pubKeyHash = psbtPubKeyHash
- } else {
- // Cannot derive PSBT key - keyshare mismatch
- return "", fmt.Errorf("input %d: keyshare mismatch - PSBT key matches script but cannot be derived from keyshare (PSBT key: %x, derived key: %x, path: %s)", i, psbtPubKeyBytes[:min(8, len(psbtPubKeyBytes))], verifyPubKeyBytes[:min(8, len(verifyPubKeyBytes))], inputDerivePath)
- }
- }
- }
-
+ // Verify the public key hash matches the script (we already verified pubkey ownership above)
if !bytes.Equal(scriptPubKeyHash, pubKeyHash) {
return "", fmt.Errorf("public key hash mismatch for input %d: script expects %x but got %x (derived from path: %s)", i, scriptPubKeyHash, pubKeyHash, inputDerivePath)
}
@@ -900,6 +872,10 @@ func runNostrMpcSignPSBTInternal(
inputDerivePath = extractedPath
Logf("Input %d: Using derivation path from PSBT Bip32Derivation: %s", i, inputDerivePath)
+ // Get the pubkey from PSBT's Bip32Derivation (this is what the PSBT expects)
+ psbtPubKeyBytes := input.Bip32Derivation[0].PubKey
+ Logf("Input %d: PSBT Bip32Derivation pubkey: %x", i, psbtPubKeyBytes)
+
// Derive the public key from keyshare using GetDerivedPubKey with the path from PSBT
// Use the keyshare's pub_key and chaincode to derive the corresponding public key
Logf("Input %d: Deriving public key from keyshare - xpub: %s, chaincode: %s, path: %s", i, truncateHex(keyshareData.PubKey), truncateHex(keyshareData.ChainCodeHex), inputDerivePath)
@@ -913,7 +889,15 @@ func runNostrMpcSignPSBTInternal(
}
Logf("Input %d: Derived public key (hex): %s, bytes: %x (length: %d)", i, truncateHex(derivedPubKeyHex), derivedPubKeyBytes, len(derivedPubKeyBytes))
- // Use the derived key for signing
+ // Check if this input belongs to us by comparing derived pubkey with PSBT's pubkey
+ if !bytes.Equal(derivedPubKeyBytes, psbtPubKeyBytes) {
+ Logf("Input %d: ⚠️ SKIPPING SIGNATURE - Pubkey mismatch: derived %x != PSBT %x (path: %s)", i, derivedPubKeyBytes[:min(8, len(derivedPubKeyBytes))], psbtPubKeyBytes[:min(8, len(psbtPubKeyBytes))], inputDerivePath)
+ Logf("Input %d: This input does not belong to our wallet, skipping signature", i)
+ continue // Skip signing this input, but continue with other inputs
+ }
+
+ Logf("Input %d: ✅ Pubkey matches - this input belongs to our wallet, will sign", i)
+ // Use the derived key for signing (which matches PSBT's key)
inputPubKeyBytes = derivedPubKeyBytes
// Create session for this input
@@ -949,17 +933,7 @@ func runNostrMpcSignPSBTInternal(
Logf("Input %d: Derived pubkey hash: %x", i, pubKeyHash)
Logf("Input %d: Derived pubkey: %x", i, inputPubKeyBytes)
- // Check PSBT's Bip32Derivation key if available
- if len(input.Bip32Derivation) > 0 {
- psbtPubKeyBytes := input.Bip32Derivation[0].PubKey
- psbtPubKeyHash := btcutil.Hash160(psbtPubKeyBytes)
- Logf("Input %d: PSBT Bip32Derivation pubkey: %x", i, psbtPubKeyBytes)
- Logf("Input %d: PSBT Bip32Derivation pubkey hash: %x", i, psbtPubKeyHash)
- if bytes.Equal(scriptPubKeyHash, psbtPubKeyHash) {
- Logf("Input %d: PSBT key matches script, but derived key doesn't - keyshare mismatch?", i)
- }
- }
-
+ // Verify the public key hash matches the script (we already verified pubkey ownership above)
if !bytes.Equal(scriptPubKeyHash, pubKeyHash) {
return "", fmt.Errorf("public key hash mismatch for input %d: script expects %x but got %x (derived from path: %s)", i, scriptPubKeyHash, pubKeyHash, inputDerivePath)
}
@@ -1024,37 +998,7 @@ func runNostrMpcSignPSBTInternal(
Logf("Input %d: Derived pubkey hash: %x", i, pubKeyHash)
Logf("Input %d: Derived pubkey: %x", i, inputPubKeyBytes)
- // Check PSBT's Bip32Derivation key if available
- if len(input.Bip32Derivation) > 0 {
- psbtPubKeyBytes := input.Bip32Derivation[0].PubKey
- psbtPubKeyHash := btcutil.Hash160(psbtPubKeyBytes)
- Logf("Input %d: PSBT Bip32Derivation pubkey: %x", i, psbtPubKeyBytes)
- Logf("Input %d: PSBT Bip32Derivation pubkey hash: %x", i, psbtPubKeyHash)
-
- // If derived key doesn't match script, but PSBT key does, verify we can derive PSBT key
- if !bytes.Equal(scriptPubKeyHash, pubKeyHash) && bytes.Equal(scriptPubKeyHash, psbtPubKeyHash) {
- Logf("Input %d: PSBT key matches script, but derived key doesn't - verifying if PSBT key can be derived from keyshare", i)
- // Try to derive the PSBT key from our keyshare
- verifyPubKeyHex, err := GetDerivedPubKey(keyshareData.PubKey, keyshareData.ChainCodeHex, inputDerivePath, false)
- if err != nil {
- return "", fmt.Errorf("input %d: failed to verify PSBT key derivation: %w", i, err)
- }
- verifyPubKeyBytes, err := hex.DecodeString(verifyPubKeyHex)
- if err != nil {
- return "", fmt.Errorf("input %d: failed to decode verified public key: %w", i, err)
- }
- if bytes.Equal(verifyPubKeyBytes, psbtPubKeyBytes) {
- // We can derive it - use the PSBT key
- Logf("Input %d: Verified PSBT key can be derived from keyshare - using PSBT key", i)
- inputPubKeyBytes = psbtPubKeyBytes
- pubKeyHash = psbtPubKeyHash
- } else {
- // Cannot derive PSBT key - keyshare mismatch
- return "", fmt.Errorf("input %d: keyshare mismatch - PSBT key matches script but cannot be derived from keyshare (PSBT key: %x, derived key: %x, path: %s)", i, psbtPubKeyBytes[:min(8, len(psbtPubKeyBytes))], verifyPubKeyBytes[:min(8, len(verifyPubKeyBytes))], inputDerivePath)
- }
- }
- }
-
+ // Verify the public key hash matches the script (we already verified pubkey ownership above)
if !bytes.Equal(scriptPubKeyHash, pubKeyHash) {
return "", fmt.Errorf("public key hash mismatch for input %d: script expects %x but got %x (derived from path: %s)", i, scriptPubKeyHash, pubKeyHash, inputDerivePath)
}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d7ec4ca..c6ea984 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,72 @@
## [Unreleased]
+## [2.1.4] - 2025-12-30
+
+### Added
+- **Resilient Nostr relay connections**: BTC sends now work reliably even if some Nostr relays are down
+ - Automatically connects to multiple relays in parallel
+ - Continues working if relays fail during the signing process
+ - Faster connection establishment and better error recovery
+
+### Changed
+- **Faster pre-agreement timeout**: Reduced from 2 minutes to 16 seconds for quicker failure detection
+
+### Fixed
+- **Android navigation bar overlap**: Fixed bottom navigation bar overlapping system navigation on Android devices (e.g., Samsung)
+- **Message delivery reliability**: Improved handling of messages sent just before subscription starts
+
+---
+
+## [2.1.3] - 2025-12-20
+
+### Added
+- **Hide/show balance user preference**: Tap balance to toggle visibility, preference persists across app sessions
+- **Unified QR Scanner component** (`components/QRScanner.tsx` and `components/QRScanner.foss.tsx`):
+ - Platform-specific implementations: iOS uses `react-native-vision-camera`, Android uses `rn-barcode-zxing-scan`
+ - Support for both single and continuous scanning modes
+ - Progress indicator for animated QR codes
+ - Customizable titles, subtitles, and close button text
+- **New `useQRScanner` hook** for reusable QR scanning logic
+- **Enhanced pubkey matching logic** in `BBMTLib/tss/psbt.go`:
+ - Better validation of input ownership before signing
+ - Improved logging for debugging signature issues
+ - Skips signing inputs that don't belong to the wallet (prevents errors)
+ - More reliable pubkey derivation verification
+- **PSBT sign filter**: Automatically filters and only signs inputs that belong to the wallet, preventing errors from unrelated inputs
+- **QR Scan shortcut for Send (Second Device)**: Added QR code option in transport mode selector to share transaction details (address, amount, fee) from one phone to another for quick entry
+
+### Changed
+- **Wallet mode terminology**: Updated from "basic/flexi" to "duo/trio" for clearer wallet type indication (2/2 vs 2/3 multisig)
+- **Keyshare backup naming**: Keyshare backup files now use xpub-based naming with index (e.g., `a6tr2-1.ks`, `a6tr2-2.ks`, `a6tr2-3.ks`) for better organization and identification
+- **Default address type**: New wallets default to SegWit Native (BIP84) instead of Legacy addresses for better efficiency and lower fees
+- **Refactored screens** to use new QR scanner:
+ - `MobileNostrPairing.tsx` - Simplified QR scanning integration
+ - `SendBitcoinModal.tsx` - Improved QR code scanning UX
+ - `PSBTModal.tsx` - Enhanced QR code handling
+ - `WalletHome.tsx` - Updated QR scanning flow
+- **Code cleanup**: Removed large FOSS variant files (reduced codebase by ~8,500 lines):
+ - `screens/MobileNostrPairing.foss.tsx` (6,032 lines removed)
+ - `screens/PSBTModal.foss.tsx` (1,661 lines removed)
+ - `screens/SendBitcoinModal.foss.tsx` (782 lines removed)
+
+### Fixed
+- **Wallet Context & Address Handling**: Improved address type handling with legacy wallet detection
+- **Derive path management**: Better derive path management for different network types
+- **Address type caching**: Enhanced address type caching and retrieval
+- **Build & Dependencies**: Updated Android build configuration and TSS library binaries (iOS and Android)
+- **Release script**: Improved release script (`android/release.sh`)
+
+### Technical Details
+- **Components**: Updated `KeyshareModal.tsx`, `TransportModeSelector.tsx`, and `Styles.tsx`
+- **Screens**: Improvements to `WalletSettings.tsx`, `ShowcaseScreen.tsx`, and `PSBTScreen.tsx`
+- **Utilities**: New utility functions in `utils.js` (53 lines added) with improved error handling and logging
+- **Native Libraries**: Updated TSS framework binaries for iOS (all architectures) and TSS AAR library for Android
+- **Native headers**: Updated native headers and Info.plist files
+- **Statistics**: 32 files changed, 1,570 insertions, 9,089 deletions (net reduction of ~7,500 lines)
+
+---
+
## [2.1.2] - 2025-12-19
### Added
diff --git a/Dockerfile b/Dockerfile
index 36a619c..03dcacb 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -52,9 +52,7 @@ WORKDIR /BoldWallet
# conditional F-Droid build switch
RUN if [ "$fdroid" = "true" ]; then \
sed -i '/react-native-vision-camera/d' package.json; \
- mv /BoldWallet/screens/SendBitcoinModal.foss.tsx /BoldWallet/screens/SendBitcoinModal.tsx; \
- mv /BoldWallet/screens/MobileNostrPairing.foss.tsx /BoldWallet/screens/MobileNostrPairing.tsx; \
- mv /BoldWallet/screens/PSBTModal.foss.tsx /BoldWallet/screens/PSBTModal.tsx; \
+ mv /BoldWallet/components/QRScanner.foss.tsx /BoldWallet/components/QRScanner.tsx; \
fi
# npm install
diff --git a/android/app/build.gradle b/android/app/build.gradle
index d2df81c..49a3c14 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -24,8 +24,8 @@ android {
applicationId "com.boldwallet"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
- versionCode 35
- versionName "2.1.2"
+ versionCode 37
+ versionName "2.1.4"
missingDimensionStrategy 'react-native-camera', 'general'
missingDimensionStrategy 'react-native-arch', 'oldarch'
diff --git a/android/app/libs/tss-sources.jar b/android/app/libs/tss-sources.jar
index f2aeb24..7d1ec39 100644
Binary files a/android/app/libs/tss-sources.jar and b/android/app/libs/tss-sources.jar differ
diff --git a/android/app/libs/tss.aar b/android/app/libs/tss.aar
index f273922..136c95b 100644
Binary files a/android/app/libs/tss.aar and b/android/app/libs/tss.aar differ
diff --git a/android/release.sh b/android/release.sh
index 3fc3c87..f72dd09 100755
--- a/android/release.sh
+++ b/android/release.sh
@@ -1,12 +1,15 @@
#!/bin/bash
# Script to automate generating a release APK for React Native
+# This script generates a DEV keystore for local development builds.
+# For production releases, use signed-release.sh with production credentials.
-# Keystore details (modify these with your own values)
-KEYSTORE_FILE="my-release-key.jks"
-KEY_ALIAS="my-key"
-KEYSTORE_PASSWORD="your_actual_password_here"
-KEY_PASSWORD="your_actual_password_here"
+# Dev keystore details (for local development only)
+# Note: Modern Java uses PKCS12 format which requires same password for store and key
+KEYSTORE_FILE="dev-release-key.jks"
+KEY_ALIAS="dev-key"
+KEYSTORE_PASSWORD="dev-keystore-password"
+KEY_PASSWORD="dev-keystore-password" # Must match KEYSTORE_PASSWORD for PKCS12
# Paths
KEYSTORE_PATH="app/$KEYSTORE_FILE"
@@ -14,62 +17,78 @@ GRADLE_PROPERTIES_PATH="release.properties"
echo -e "--- Starting React Native APK Release Build Automation ---"
-# Step 1: Generate Keystore if it doesn't exist
-if [ ! -f "$KEYSTORE_PATH" ]; then
- echo -e "Generating new Keystore..."
+# Step 1: Generate or verify Dev Keystore
+KEYSTORE_VALID=false
+
+if [ -f "$KEYSTORE_PATH" ]; then
+ echo -e "Dev keystore exists. Verifying it works with current credentials..."
+ # Try to list the keystore to verify password works
+ if keytool -list -keystore "$KEYSTORE_PATH" -storepass "$KEYSTORE_PASSWORD" -alias "$KEY_ALIAS" >/dev/null 2>&1; then
+ KEYSTORE_VALID=true
+ echo -e "✅ Existing dev keystore is valid."
+ else
+ echo -e "⚠️ Existing keystore doesn't work with current credentials."
+ echo -e "🗑️ Removing old keystore to regenerate..."
+ rm -f "$KEYSTORE_PATH"
+ KEYSTORE_VALID=false
+ fi
+fi
+
+if [ "$KEYSTORE_VALID" = false ]; then
+ echo -e "Generating new DEV keystore for local development..."
+ echo -e "⚠️ This is a DEV keystore - NOT for production releases!"
keytool -genkey -v -keystore "$KEYSTORE_PATH" \
-keyalg RSA -keysize 2048 -validity 10000 -alias "$KEY_ALIAS" \
- -dname "CN=Unknown, OU=Unknown, O=Unknown, L=Unknown, ST=Unknown, C=Unknown" \
- -storepass "$KEY_PASSWORD" -keypass "$KEY_PASSWORD"
+ -dname "CN=Dev, OU=Dev, O=Dev, L=Dev, ST=Dev, C=US" \
+ -storepass "$KEYSTORE_PASSWORD" -keypass "$KEY_PASSWORD" 2>&1
- echo -e "Keystore generated at: $KEYSTORE_PATH"
-else
- echo -e "Keystore already exists. Skipping generation."
+ if [ $? -eq 0 ]; then
+ echo -e "✅ Dev keystore generated at: $KEYSTORE_PATH"
+ echo -e "⚠️ This keystore is for development/testing only!"
+ else
+ echo -e "❌ Failed to generate keystore!"
+ exit 1
+ fi
fi
-# Step 2: Update *.properties with Keystore credentials
+# Step 2: Update release.properties with Keystore credentials
if [ ! -f "$GRADLE_PROPERTIES_PATH" ]; then
echo -e "Creating release.properties file..."
touch "$GRADLE_PROPERTIES_PATH"
fi
-# Check if the store file path is correct, update if needed
-if grep -q "MYAPP_UPLOAD_STORE_FILE" "$GRADLE_PROPERTIES_PATH"; then
- CURRENT_PATH=$(grep "MYAPP_UPLOAD_STORE_FILE" "$GRADLE_PROPERTIES_PATH" | cut -d'=' -f2)
- if [ "$CURRENT_PATH" != "$KEYSTORE_PATH" ]; then
- echo -e "Updating incorrect keystore path in release.properties..."
- # Update the path using sed
+# Function to update or add property
+update_property() {
+ local key=$1
+ local value=$2
+ if grep -q "^${key}=" "$GRADLE_PROPERTIES_PATH"; then
+ # Update existing property
if [[ "$OSTYPE" == "darwin"* ]]; then
# macOS
- sed -i '' "s|MYAPP_UPLOAD_STORE_FILE=.*|MYAPP_UPLOAD_STORE_FILE=$KEYSTORE_PATH|" "$GRADLE_PROPERTIES_PATH"
+ sed -i '' "s|^${key}=.*|${key}=${value}|" "$GRADLE_PROPERTIES_PATH"
else
# Linux
- sed -i "s|MYAPP_UPLOAD_STORE_FILE=.*|MYAPP_UPLOAD_STORE_FILE=$KEYSTORE_PATH|" "$GRADLE_PROPERTIES_PATH"
+ sed -i "s|^${key}=.*|${key}=${value}|" "$GRADLE_PROPERTIES_PATH"
fi
- echo -e "Updated MYAPP_UPLOAD_STORE_FILE to: $KEYSTORE_PATH"
+ echo -e "Updated ${key} in release.properties"
else
- echo -e "Keystore path in release.properties is correct."
+ # Add new property
+ echo "${key}=${value}" >> "$GRADLE_PROPERTIES_PATH"
+ echo -e "Added ${key} to release.properties"
fi
-
- # Update other properties if they don't exist or are incorrect
- if ! grep -q "MYAPP_UPLOAD_KEY_ALIAS" "$GRADLE_PROPERTIES_PATH"; then
- echo "MYAPP_UPLOAD_KEY_ALIAS=$KEY_ALIAS" >> "$GRADLE_PROPERTIES_PATH"
- fi
- if ! grep -q "MYAPP_UPLOAD_STORE_PASSWORD" "$GRADLE_PROPERTIES_PATH"; then
- echo "MYAPP_UPLOAD_STORE_PASSWORD=$KEYSTORE_PASSWORD" >> "$GRADLE_PROPERTIES_PATH"
- fi
- if ! grep -q "MYAPP_UPLOAD_KEY_PASSWORD" "$GRADLE_PROPERTIES_PATH"; then
- echo "MYAPP_UPLOAD_KEY_PASSWORD=$KEY_PASSWORD" >> "$GRADLE_PROPERTIES_PATH"
- fi
-else
- echo -e "Adding Keystore configuration to release.properties..."
- cat <> $GRADLE_PROPERTIES_PATH
-MYAPP_UPLOAD_STORE_FILE=$KEYSTORE_PATH
-MYAPP_UPLOAD_KEY_ALIAS=$KEY_ALIAS
-MYAPP_UPLOAD_STORE_PASSWORD=$KEYSTORE_PASSWORD
-MYAPP_UPLOAD_KEY_PASSWORD=$KEY_PASSWORD
-EOL
-fi
+}
+
+# Update all keystore properties
+echo -e "Updating DEV keystore configuration in release.properties..."
+update_property "MYAPP_UPLOAD_STORE_FILE" "$KEYSTORE_PATH"
+update_property "MYAPP_UPLOAD_KEY_ALIAS" "$KEY_ALIAS"
+update_property "MYAPP_UPLOAD_STORE_PASSWORD" "$KEYSTORE_PASSWORD"
+update_property "MYAPP_UPLOAD_KEY_PASSWORD" "$KEY_PASSWORD"
+
+echo -e ""
+echo -e "⚠️ NOTE: This build uses a DEV keystore for local development."
+echo -e "⚠️ For production releases, use signed-release.sh instead."
+echo -e ""
# Step 3: Build the Release APK
echo -e "Building the Release APK..."
diff --git a/components/KeyshareModal.tsx b/components/KeyshareModal.tsx
index ae6bf40..b246c85 100644
--- a/components/KeyshareModal.tsx
+++ b/components/KeyshareModal.tsx
@@ -22,7 +22,7 @@ interface KeyshareInfo {
label: string;
supportsLocal: boolean;
supportsNostr: boolean;
- type: 'basic' | 'flexi';
+ type: 'duo' | 'trio';
pubKey: string;
chainCode: string;
xpub: string;
@@ -282,14 +282,14 @@ const KeyshareModal: React.FC = ({
- {keyshareInfo.type === 'flexi'
- ? 'Flexi • 3 devices'
- : 'Basic • 2 devices'}
+ {keyshareInfo.type === 'trio'
+ ? 'Trio • 3 devices'
+ : 'Duo • 2 devices'}
diff --git a/components/QRScanner.foss.tsx b/components/QRScanner.foss.tsx
new file mode 100644
index 0000000..72b66de
--- /dev/null
+++ b/components/QRScanner.foss.tsx
@@ -0,0 +1,298 @@
+import React, {useState, useEffect, useRef, useCallback} from 'react';
+import {
+ View,
+ Text,
+ TouchableOpacity,
+ StyleSheet,
+ Modal,
+ Platform,
+} from 'react-native';
+import BarcodeZxingScan from 'rn-barcode-zxing-scan';
+import {useTheme} from '../theme';
+import {dbg} from '../utils';
+
+export interface QRProgress {
+ received: number;
+ total: number;
+ percentage?: number;
+}
+
+export interface QRScannerProps {
+ visible: boolean;
+ onClose: () => void;
+ onScan: (data: string) => void;
+ mode?: 'single' | 'continuous';
+ title?: string;
+ subtitle?: string;
+ showProgress?: boolean;
+ progress?: QRProgress;
+ closeButtonText?: string;
+}
+
+const QRScanner: React.FC = ({
+ visible,
+ onClose,
+ onScan,
+ mode = 'single',
+ title,
+ subtitle,
+ showProgress = false,
+ progress,
+ closeButtonText = 'Close',
+}) => {
+ const {theme} = useTheme();
+ const [isScanning, setIsScanning] = useState(false);
+ const scanSubscriptionRef = useRef(null);
+
+ const styles = StyleSheet.create({
+ modalOverlay: {
+ flex: 1,
+ backgroundColor: 'black',
+ },
+ scannerContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ backgroundColor: 'black',
+ },
+ qrFrame: {
+ width: 250,
+ height: 250,
+ borderWidth: 2,
+ borderColor: theme.colors.primary,
+ borderRadius: 12,
+ position: 'absolute',
+ alignSelf: 'center',
+ top: '25%',
+ },
+ scannerHeader: {
+ position: 'absolute',
+ top: 80,
+ left: 0,
+ right: 0,
+ alignItems: 'center',
+ },
+ scannerTitle: {
+ fontSize: 20,
+ fontWeight: '700',
+ color: '#FFFFFF',
+ marginBottom: 8,
+ },
+ scannerSubtitle: {
+ fontSize: 14,
+ color: 'rgba(255, 255, 255, 0.7)',
+ textAlign: 'center',
+ paddingHorizontal: 20,
+ },
+ progressBarContainer: {
+ marginTop: 16,
+ width: 200,
+ height: 6,
+ backgroundColor: 'rgba(255, 255, 255, 0.2)',
+ borderRadius: 3,
+ overflow: 'hidden',
+ },
+ progressBar: {
+ height: '100%',
+ backgroundColor: '#F7931A',
+ borderRadius: 3,
+ },
+ closeScannerButton: {
+ position: 'absolute',
+ bottom: 60,
+ alignSelf: 'center',
+ backgroundColor: theme.colors.primary,
+ paddingHorizontal: 24,
+ paddingVertical: 12,
+ borderRadius: 12,
+ },
+ closeScannerButtonText: {
+ color: theme.colors.background,
+ fontSize: 16,
+ fontWeight: '600',
+ },
+ cameraNotFoundContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ backgroundColor: 'black',
+ },
+ cameraNotFoundText: {
+ color: '#FFFFFF',
+ fontSize: 16,
+ marginBottom: 8,
+ },
+ cameraNotFoundSubtext: {
+ color: 'rgba(255, 255, 255, 0.7)',
+ fontSize: 14,
+ textAlign: 'center',
+ paddingHorizontal: 20,
+ },
+ });
+
+ // Handle continuous scanning for Android
+ useEffect(() => {
+ if (visible && mode === 'continuous' && Platform.OS === 'android') {
+ // Set up event listener for continuous scanning
+ const {DeviceEventEmitter} = require('react-native');
+ scanSubscriptionRef.current = DeviceEventEmitter.addListener(
+ 'BarcodeZxingScanContinuous',
+ (event: {data?: string; error?: string}) => {
+ if (event.error) {
+ dbg('FOSS: Continuous scan error:', event.error);
+ setIsScanning(false);
+ return;
+ }
+ if (event.data) {
+ dbg('FOSS: Continuous scan result:', event.data.substring(0, 50));
+ onScan(event.data);
+ }
+ },
+ );
+
+ // Start continuous scan
+ BarcodeZxingScan.showQrReaderContinuous((error: any, data: any) => {
+ if (error) {
+ dbg('FOSS: Continuous scan error:', error);
+ setIsScanning(false);
+ return;
+ }
+ if (data === 'SCANNER_STARTED') {
+ setIsScanning(true);
+ if (showProgress) {
+ setTimeout(() => {
+ BarcodeZxingScan.updateProgressText(
+ title || 'Scanning QR Code...',
+ );
+ }, 100);
+ }
+ }
+ });
+ }
+
+ return () => {
+ if (scanSubscriptionRef.current) {
+ scanSubscriptionRef.current.remove();
+ scanSubscriptionRef.current = null;
+ }
+ if (mode === 'continuous' && Platform.OS === 'android' && isScanning) {
+ BarcodeZxingScan.stopContinuousScan();
+ setIsScanning(false);
+ }
+ };
+ }, [visible, mode, onScan, showProgress, title, isScanning]);
+
+ // Handle single scan
+ const handleSingleScan = useCallback(() => {
+ // Set custom status message before opening scanner (if supported)
+ if (subtitle && BarcodeZxingScan.setStatusMessage) {
+ BarcodeZxingScan.setStatusMessage(subtitle);
+ }
+
+ if (Platform.OS === 'android') {
+ BarcodeZxingScan.showQrReader((error: any, data: any) => {
+ // Clear custom status message
+ if (BarcodeZxingScan.setStatusMessage) {
+ BarcodeZxingScan.setStatusMessage('');
+ }
+ if (error) {
+ dbg('FOSS: Single scan error:', error);
+ return;
+ }
+ if (data) {
+ onScan(data);
+ onClose();
+ }
+ });
+ } else {
+ // iOS - use single scan
+ BarcodeZxingScan.showQrReader((error: any, data: any) => {
+ // Clear custom status message
+ if (BarcodeZxingScan.setStatusMessage) {
+ BarcodeZxingScan.setStatusMessage('');
+ }
+ if (error) {
+ dbg('FOSS: iOS scan error:', error);
+ return;
+ }
+ if (data) {
+ onScan(data);
+ onClose();
+ }
+ });
+ }
+ }, [subtitle, onScan, onClose]);
+
+ // Auto-start single scan when modal opens
+ useEffect(() => {
+ if (visible && mode === 'single' && !isScanning) {
+ handleSingleScan();
+ }
+ }, [visible, mode, isScanning, handleSingleScan]);
+
+ const isAnimatedQR = showProgress && progress && progress.total > 1;
+ const progressPercent = isAnimatedQR
+ ? Math.min(
+ 100,
+ progress.percentage ||
+ Math.round((progress.received / progress.total) * 100),
+ )
+ : 0;
+ const isComplete = isAnimatedQR && progress.received >= progress.total;
+
+ const displayTitle = title || (isAnimatedQR ? 'Scanning Animated QR...' : 'Scan QR Code');
+ const displaySubtitle =
+ subtitle ||
+ (isAnimatedQR
+ ? isComplete
+ ? 'Processing...'
+ : `Keep scanning animated QR: ${progressPercent}%`
+ : 'Point camera at the QR code to scan');
+
+ // For continuous mode on Android, show the scanner UI
+ if (mode === 'continuous' && Platform.OS === 'android' && visible) {
+ return (
+
+
+
+ {(title || subtitle || showProgress) && (
+
+ {displayTitle}
+ {displaySubtitle}
+ {isAnimatedQR && (
+
+
+
+ )}
+
+ )}
+ {
+ if (isScanning) {
+ BarcodeZxingScan.stopContinuousScan();
+ setIsScanning(false);
+ }
+ onClose();
+ }}
+ activeOpacity={0.7}>
+ {closeButtonText}
+
+
+
+ );
+ }
+
+ // For single mode, the native scanner handles UI, but we show a placeholder
+ // In practice, single mode opens native scanner which handles its own UI
+ return null;
+};
+
+export default QRScanner;
+
diff --git a/components/QRScanner.tsx b/components/QRScanner.tsx
new file mode 100644
index 0000000..752ddd9
--- /dev/null
+++ b/components/QRScanner.tsx
@@ -0,0 +1,524 @@
+import React, {useState, useEffect, useRef} from 'react';
+import {
+ View,
+ Text,
+ TouchableOpacity,
+ StyleSheet,
+ Modal,
+ Platform,
+} from 'react-native';
+import {useTheme} from '../theme';
+import {dbg} from '../utils';
+
+// iOS-specific imports (only imported when needed)
+let Camera: any = null;
+let useCameraDevice: any = null;
+let useCodeScanner: any = null;
+
+if (Platform.OS === 'ios') {
+ try {
+ const visionCamera = require('react-native-vision-camera');
+ Camera = visionCamera.Camera;
+ useCameraDevice = visionCamera.useCameraDevice;
+ useCodeScanner = visionCamera.useCodeScanner;
+ } catch (e) {
+ console.warn('Vision camera not available:', e);
+ }
+}
+
+// Android-specific import
+let BarcodeZxingScan: any = null;
+if (Platform.OS === 'android') {
+ try {
+ BarcodeZxingScan = require('rn-barcode-zxing-scan').default;
+ } catch (e) {
+ console.warn('BarcodeZxingScan not available:', e);
+ }
+}
+
+export interface QRProgress {
+ received: number;
+ total: number;
+ percentage?: number;
+}
+
+export interface QRScannerProps {
+ visible: boolean;
+ onClose: () => void;
+ onScan: (data: string) => void;
+ mode?: 'single' | 'continuous';
+ title?: string;
+ subtitle?: string;
+ showProgress?: boolean;
+ progress?: QRProgress;
+ closeButtonText?: string;
+}
+
+// iOS QR Scanner Component (uses vision-camera)
+const IOSQRScanner: React.FC = ({
+ visible,
+ onClose,
+ onScan,
+ mode = 'single',
+ title,
+ subtitle,
+ showProgress = false,
+ progress,
+ closeButtonText = 'Close',
+}) => {
+ const {theme} = useTheme();
+ const device = useCameraDevice('back');
+ const codeScanner = useCodeScanner({
+ codeTypes: ['qr'],
+ onCodeScanned: codes => {
+ if (codes.length > 0 && codes[0].value) {
+ onScan(codes[0].value);
+ if (mode === 'single') {
+ onClose();
+ }
+ }
+ },
+ });
+
+ const styles = StyleSheet.create({
+ scannerContainer: {
+ flex: 1,
+ backgroundColor: 'black',
+ },
+ qrFrame: {
+ position: 'absolute',
+ borderWidth: 2,
+ borderColor: theme.colors.primary,
+ width: 250,
+ height: 250,
+ alignSelf: 'center',
+ top: '25%',
+ borderRadius: 12,
+ },
+ scannerHeader: {
+ position: 'absolute',
+ top: 80,
+ left: 0,
+ right: 0,
+ alignItems: 'center',
+ },
+ scannerTitle: {
+ fontSize: 20,
+ fontWeight: '700',
+ color: '#FFFFFF',
+ marginBottom: 8,
+ },
+ scannerSubtitle: {
+ fontSize: 14,
+ color: 'rgba(255, 255, 255, 0.7)',
+ textAlign: 'center',
+ paddingHorizontal: 20,
+ },
+ progressBarContainer: {
+ marginTop: 16,
+ width: 200,
+ height: 6,
+ backgroundColor: 'rgba(255, 255, 255, 0.2)',
+ borderRadius: 3,
+ overflow: 'hidden',
+ },
+ progressBar: {
+ height: '100%',
+ backgroundColor: '#F7931A',
+ borderRadius: 3,
+ },
+ closeScannerButton: {
+ position: 'absolute',
+ bottom: 60,
+ alignSelf: 'center',
+ backgroundColor: theme.colors.primary,
+ paddingHorizontal: 24,
+ paddingVertical: 12,
+ borderRadius: 12,
+ },
+ closeScannerButtonText: {
+ color: theme.colors.background,
+ fontSize: 16,
+ fontWeight: '600',
+ },
+ cameraNotFoundContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ backgroundColor: 'black',
+ },
+ cameraNotFoundText: {
+ color: '#FFFFFF',
+ fontSize: 16,
+ marginBottom: 8,
+ },
+ cameraNotFoundSubtext: {
+ color: 'rgba(255, 255, 255, 0.7)',
+ fontSize: 14,
+ textAlign: 'center',
+ paddingHorizontal: 20,
+ },
+ });
+
+ if (!device || !codeScanner) {
+ return (
+
+
+ Camera Not Available
+
+ Please check camera permissions in Settings
+
+
+ {closeButtonText}
+
+
+
+ );
+ }
+
+ const isAnimatedQR = showProgress && progress && progress.total > 1;
+ const progressPercent = isAnimatedQR
+ ? Math.min(
+ 100,
+ progress.percentage ||
+ Math.round((progress.received / progress.total) * 100),
+ )
+ : 0;
+ const isComplete = isAnimatedQR && progress.received >= progress.total;
+
+ const displayTitle = title || (isAnimatedQR ? 'Scanning Animated QR...' : 'Scan QR Code');
+ const displaySubtitle =
+ subtitle ||
+ (isAnimatedQR
+ ? isComplete
+ ? 'Processing...'
+ : `Keep scanning animated QR: ${progressPercent}%`
+ : 'Point camera at the QR code to scan');
+
+ return (
+
+
+
+
+ {(title || subtitle || showProgress) && (
+
+ {displayTitle}
+ {displaySubtitle}
+ {isAnimatedQR && (
+
+
+
+ )}
+
+ )}
+
+ {closeButtonText}
+
+
+
+ );
+};
+
+// Android QR Scanner Component (uses BarcodeZxingScan)
+const AndroidQRScanner: React.FC = ({
+ visible,
+ onClose,
+ onScan,
+ mode = 'single',
+ title,
+ subtitle,
+ showProgress = false,
+ progress,
+ closeButtonText = 'Close',
+}) => {
+ const {theme} = useTheme();
+ const [isScanning, setIsScanning] = useState(false);
+ const scanSubscriptionRef = useRef(null);
+ const isScanningRef = useRef(false);
+
+ const styles = StyleSheet.create({
+ scannerContainer: {
+ flex: 1,
+ justifyContent: 'center',
+ alignItems: 'center',
+ backgroundColor: 'black',
+ },
+ qrFrame: {
+ width: 250,
+ height: 250,
+ borderWidth: 2,
+ borderColor: theme.colors.primary,
+ borderRadius: 12,
+ position: 'absolute',
+ alignSelf: 'center',
+ top: '25%',
+ },
+ scannerHeader: {
+ position: 'absolute',
+ top: 80,
+ left: 0,
+ right: 0,
+ alignItems: 'center',
+ },
+ scannerTitle: {
+ fontSize: 20,
+ fontWeight: '700',
+ color: '#FFFFFF',
+ marginBottom: 8,
+ },
+ scannerSubtitle: {
+ fontSize: 14,
+ color: 'rgba(255, 255, 255, 0.7)',
+ textAlign: 'center',
+ paddingHorizontal: 20,
+ },
+ progressBarContainer: {
+ marginTop: 16,
+ width: 200,
+ height: 6,
+ backgroundColor: 'rgba(255, 255, 255, 0.2)',
+ borderRadius: 3,
+ overflow: 'hidden',
+ },
+ progressBar: {
+ height: '100%',
+ backgroundColor: '#F7931A',
+ borderRadius: 3,
+ },
+ closeScannerButton: {
+ position: 'absolute',
+ bottom: 60,
+ alignSelf: 'center',
+ backgroundColor: theme.colors.primary,
+ paddingHorizontal: 24,
+ paddingVertical: 12,
+ borderRadius: 12,
+ },
+ closeScannerButtonText: {
+ color: theme.colors.background,
+ fontSize: 16,
+ fontWeight: '600',
+ },
+ });
+
+ // Handle scanner close - properly stop scanner and cleanup
+ const handleClose = () => {
+ if (mode === 'continuous' && isScanningRef.current && BarcodeZxingScan) {
+ dbg('Android: Stopping continuous scan (close requested)');
+ BarcodeZxingScan.stopContinuousScan();
+ BarcodeZxingScan.updateProgressText('');
+ isScanningRef.current = false;
+ setIsScanning(false);
+ }
+ if (scanSubscriptionRef.current) {
+ scanSubscriptionRef.current.remove();
+ scanSubscriptionRef.current = null;
+ }
+ onClose();
+ };
+
+ // Handle continuous scanning for Android
+ useEffect(() => {
+ if (visible && mode === 'continuous' && BarcodeZxingScan) {
+ // Only start if not already scanning
+ if (!isScanningRef.current) {
+ const {DeviceEventEmitter} = require('react-native');
+ scanSubscriptionRef.current = DeviceEventEmitter.addListener(
+ 'BarcodeZxingScanContinuous',
+ (event: {data?: string; error?: string}) => {
+ if (event.error) {
+ dbg('Android: Continuous scan error:', event.error);
+ isScanningRef.current = false;
+ setIsScanning(false);
+ return;
+ }
+ if (event.data) {
+ dbg('Android: Continuous scan result:', event.data.substring(0, 50));
+ onScan(event.data);
+ }
+ },
+ );
+
+ BarcodeZxingScan.showQrReaderContinuous((error: any, data: any) => {
+ if (error) {
+ dbg('Android: Continuous scan error:', error);
+ isScanningRef.current = false;
+ setIsScanning(false);
+ return;
+ }
+ if (data === 'SCANNER_STARTED') {
+ isScanningRef.current = true;
+ setIsScanning(true);
+ if (showProgress) {
+ setTimeout(() => {
+ BarcodeZxingScan.updateProgressText(
+ title || 'Scanning QR Code...',
+ );
+ }, 100);
+ }
+ }
+ });
+ }
+ } else if (!visible && mode === 'continuous' && BarcodeZxingScan) {
+ // Stop scanner when modal becomes invisible
+ if (isScanningRef.current) {
+ dbg('Android: Stopping continuous scan (modal closed)');
+ BarcodeZxingScan.stopContinuousScan();
+ BarcodeZxingScan.updateProgressText('');
+ isScanningRef.current = false;
+ setIsScanning(false);
+ }
+ if (scanSubscriptionRef.current) {
+ scanSubscriptionRef.current.remove();
+ scanSubscriptionRef.current = null;
+ }
+ }
+
+ return () => {
+ // Cleanup on unmount or when dependencies change
+ if (scanSubscriptionRef.current) {
+ scanSubscriptionRef.current.remove();
+ scanSubscriptionRef.current = null;
+ }
+ if (mode === 'continuous' && isScanningRef.current && BarcodeZxingScan) {
+ dbg('Android: Cleanup - stopping continuous scan');
+ BarcodeZxingScan.stopContinuousScan();
+ BarcodeZxingScan.updateProgressText('');
+ isScanningRef.current = false;
+ setIsScanning(false);
+ }
+ };
+ }, [visible, mode, onScan, showProgress, title]);
+
+ // Handle single scan - native scanner handles its own UI and back button
+ useEffect(() => {
+ if (visible && mode === 'single' && BarcodeZxingScan && !isScanningRef.current) {
+ // For single mode, native scanner opens its own activity
+ // Set custom status message before opening scanner (if supported)
+ if (subtitle && BarcodeZxingScan.setStatusMessage) {
+ BarcodeZxingScan.setStatusMessage(subtitle);
+ }
+
+ // It handles back button itself, we just need to handle the result
+ BarcodeZxingScan.showQrReader((error: any, data: any) => {
+ // Scanner closed (either via back button or scan completed)
+ isScanningRef.current = false;
+ // Clear custom status message
+ if (BarcodeZxingScan.setStatusMessage) {
+ BarcodeZxingScan.setStatusMessage('');
+ }
+ if (error) {
+ dbg('Android: Single scan error:', error);
+ // User pressed back or cancelled - close our modal too
+ onClose();
+ return;
+ }
+ if (data) {
+ onScan(data);
+ onClose();
+ } else {
+ // No data but no error - user likely pressed back
+ onClose();
+ }
+ });
+ isScanningRef.current = true;
+ } else if (!visible && mode === 'single') {
+ // Reset scanning state when modal closes
+ isScanningRef.current = false;
+ // Clear custom status message
+ if (BarcodeZxingScan && BarcodeZxingScan.setStatusMessage) {
+ BarcodeZxingScan.setStatusMessage('');
+ }
+ }
+ }, [visible, mode, onScan, onClose, subtitle]);
+
+ const isAnimatedQR = showProgress && progress && progress.total > 1;
+ const progressPercent = isAnimatedQR
+ ? Math.min(
+ 100,
+ progress.percentage ||
+ Math.round((progress.received / progress.total) * 100),
+ )
+ : 0;
+ const isComplete = isAnimatedQR && progress.received >= progress.total;
+
+ const displayTitle = title || (isAnimatedQR ? 'Scanning Animated QR...' : 'Scan QR Code');
+ const displaySubtitle =
+ subtitle ||
+ (isAnimatedQR
+ ? isComplete
+ ? 'Processing...'
+ : `Keep scanning animated QR: ${progressPercent}%`
+ : 'Point camera at the QR code to scan');
+
+ // For continuous mode, show UI overlay
+ if (mode === 'continuous' && visible) {
+ return (
+
+
+
+ {(title || subtitle || showProgress) && (
+
+ {displayTitle}
+ {displaySubtitle}
+ {isAnimatedQR && (
+
+
+
+ )}
+
+ )}
+
+ {closeButtonText}
+
+
+
+ );
+ }
+
+ // For single mode, native scanner handles UI - return null (modal is just a container)
+ // The native activity will handle its own UI and back button
+ return null;
+};
+
+// Main QRScanner component - routes to platform-specific implementation
+const QRScanner: React.FC = (props) => {
+ if (Platform.OS === 'ios') {
+ return ;
+ } else {
+ return ;
+ }
+};
+
+export default QRScanner;
diff --git a/components/Styles.tsx b/components/Styles.tsx
index f50ee88..1c49c2e 100644
--- a/components/Styles.tsx
+++ b/components/Styles.tsx
@@ -195,8 +195,8 @@ export interface Styles {
keyshareDetailValue: TextStyle;
keyshareBadge: ViewStyle;
keyshareBadgeText: TextStyle;
- keyshareBadgeFlexi: ViewStyle;
- keyshareBadgeBasic: ViewStyle;
+ keyshareBadgeTrio: ViewStyle;
+ keyshareBadgeDuo: ViewStyle;
keyshareStatusBadge: ViewStyle;
keyshareStatusBadgeText: TextStyle;
keyshareStatusBadgeSuccess: ViewStyle;
@@ -1285,10 +1285,10 @@ export const createStyles = (theme: Theme): Styles => ({
fontWeight: '600' as const,
color: '#FFFFFF',
},
- keyshareBadgeFlexi: {
+ keyshareBadgeTrio: {
backgroundColor: theme.colors.primary,
},
- keyshareBadgeBasic: {
+ keyshareBadgeDuo: {
backgroundColor: theme.colors.secondary ,
},
keyshareStatusBadge: {
diff --git a/components/TransportModeSelector.tsx b/components/TransportModeSelector.tsx
index 7ec4e3b..0933e00 100644
--- a/components/TransportModeSelector.tsx
+++ b/components/TransportModeSelector.tsx
@@ -6,9 +6,11 @@ import {
TouchableOpacity,
Modal,
Image,
+ ScrollView,
} from 'react-native';
+import QRCode from 'react-native-qrcode-svg';
import {useTheme} from '../theme';
-import {HapticFeedback} from '../utils';
+import {HapticFeedback, encodeSendBitcoinQR} from '../utils';
interface TransportModeSelectorProps {
visible: boolean;
@@ -16,6 +18,14 @@ interface TransportModeSelectorProps {
onSelect: (transport: 'local' | 'nostr') => void;
title?: string;
description?: string;
+ // Optional: Show QR code for send bitcoin data (only on device 1, not when scanned)
+ sendBitcoinData?: {
+ toAddress: string;
+ amountSats: string;
+ feeSats: string;
+ spendingHash?: string;
+ } | null;
+ showQRCode?: boolean; // Whether to show QR code (false when data came from scan)
}
const TransportModeSelector: React.FC = ({
@@ -24,6 +34,8 @@ const TransportModeSelector: React.FC = ({
onSelect,
title = 'Transport Method',
description = 'Choose how to connect with other devices',
+ sendBitcoinData = null,
+ showQRCode = true,
}) => {
const {theme} = useTheme();
const [selectedTransport, setSelectedTransport] = useState<'local' | 'nostr' | null>(null);
@@ -107,7 +119,7 @@ const TransportModeSelector: React.FC = ({
modalDescription: {
fontSize: 13,
color: theme.colors.textSecondary,
- marginBottom: 16,
+ marginBottom: 12,
textAlign: 'left',
fontWeight: '500',
},
@@ -118,9 +130,9 @@ const TransportModeSelector: React.FC = ({
},
transportOptionCard: {
borderRadius: 12,
- paddingTop: 16,
- paddingBottom: 12,
- paddingHorizontal: 12,
+ paddingTop: 12,
+ paddingBottom: 10,
+ paddingHorizontal: 10,
borderWidth: 1.5,
borderColor: theme.colors.border + '40',
backgroundColor: theme.colors.white,
@@ -141,7 +153,7 @@ const TransportModeSelector: React.FC = ({
backgroundColor: 'transparent',
},
transportOptionIconWrapper: {
- marginBottom: 8,
+ marginBottom: 6,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: 'transparent',
@@ -194,9 +206,9 @@ const TransportModeSelector: React.FC = ({
color: theme.colors.textSecondary,
},
transportSelectedHint: {
- marginTop: 14,
+ marginTop: 12,
backgroundColor: theme.colors.white,
- padding: 12,
+ padding: 10,
borderRadius: 12,
borderWidth: 1,
borderColor: theme.colors.border,
@@ -225,7 +237,7 @@ const TransportModeSelector: React.FC = ({
fontWeight: '700',
},
continueButton: {
- marginTop: 16,
+ marginTop: 12,
borderRadius: 12,
paddingVertical: 14,
alignItems: 'center',
@@ -239,6 +251,37 @@ const TransportModeSelector: React.FC = ({
fontSize: 16,
fontWeight: '600',
},
+ qrCodeSection: {
+ marginTop: 12,
+ marginBottom: 24,
+ padding: 16,
+ backgroundColor: theme.colors.white,
+ borderRadius: 12,
+ borderWidth: 1,
+ borderColor: theme.colors.border,
+ alignItems: 'center',
+ },
+ qrCodeLabel: {
+ fontSize: 13,
+ fontWeight: '600',
+ color: theme.colors.text,
+ marginBottom: 12,
+ textAlign: 'center',
+ lineHeight: 18,
+ },
+ qrCodeSubLabel: {
+ fontSize: 11,
+ fontWeight: '400',
+ color: theme.colors.textSecondary,
+ marginTop: 4,
+ textAlign: 'center',
+ lineHeight: 15,
+ },
+ qrCodeContainer: {
+ backgroundColor: 'white',
+ padding: 12,
+ borderRadius: 8,
+ },
});
return (
@@ -269,11 +312,39 @@ const TransportModeSelector: React.FC = ({
{/* Modal Body */}
-
- {description && (
+
+ {description && description.length > 0 && (
{description}
)}
+ {/* QR Code Section - Only show if sendBitcoinData exists and showQRCode is true */}
+ {sendBitcoinData && showQRCode && (() => {
+ const qrData = encodeSendBitcoinQR(
+ sendBitcoinData.toAddress,
+ sendBitcoinData.amountSats,
+ sendBitcoinData.feeSats,
+ sendBitcoinData.spendingHash || '',
+ );
+ return (
+
+
+ Quick Shortcut for Other Devices
+
+
+ Scan this QR code on other devices to{'\n'}automatically enter address and amount
+
+
+
+
+
+ );
+ })()}
+
{/* Local WiFi/Hotspot Option */}
= ({
{/* Selected Transport Hint */}
- {selectedTransport && (
+ {selectedTransport && description && description.length > 0 && (
= ({
Continue →
-
+
diff --git a/components/useQRScanner.ts b/components/useQRScanner.ts
new file mode 100644
index 0000000..c689f20
--- /dev/null
+++ b/components/useQRScanner.ts
@@ -0,0 +1,80 @@
+import {useCallback, useState} from 'react';
+import {Platform} from 'react-native';
+import BarcodeZxingScan from 'rn-barcode-zxing-scan';
+import {dbg} from '../utils';
+
+export interface UseQRScannerOptions {
+ onScan: (data: string) => void;
+ mode?: 'single' | 'continuous';
+ onError?: (error: any) => void;
+}
+
+export interface UseQRScannerReturn {
+ isScanning: boolean;
+ startScan: () => void;
+ stopScan: () => void;
+}
+
+/**
+ * Hook for handling QR scanning logic
+ * Works with both vision-camera (iOS in .tsx) and BarcodeZxingScan (Android/FOSS)
+ */
+export const useQRScanner = (
+ options: UseQRScannerOptions,
+): UseQRScannerReturn => {
+ const {onScan, mode = 'single', onError} = options;
+ const [isScanning, setIsScanning] = useState(false);
+
+ const handleScanResult = useCallback(
+ (data: string) => {
+ if (data) {
+ dbg('QR Scanner: Scanned data:', data.substring(0, 50));
+ onScan(data);
+ if (mode === 'single') {
+ setIsScanning(false);
+ }
+ }
+ },
+ [onScan, mode],
+ );
+
+ const startScan = useCallback(() => {
+ if (isScanning) {
+ dbg('QR Scanner: Already scanning, ignoring');
+ return;
+ }
+
+ setIsScanning(true);
+
+ if (Platform.OS === 'android' && mode === 'continuous') {
+ // Continuous scanning for Android (handled by component)
+ // This is mainly for reference - actual implementation in QRScanner.foss.tsx
+ dbg('QR Scanner: Starting continuous scan (Android)');
+ } else {
+ // Single scan - handled by native scanner
+ BarcodeZxingScan.showQrReader((error: any, data: any) => {
+ setIsScanning(false);
+ if (error) {
+ dbg('QR Scanner: Scan error:', error);
+ onError?.(error);
+ return;
+ }
+ handleScanResult(data);
+ });
+ }
+ }, [isScanning, mode, handleScanResult, onError]);
+
+ const stopScan = useCallback(() => {
+ if (Platform.OS === 'android' && mode === 'continuous') {
+ BarcodeZxingScan.stopQrReader();
+ }
+ setIsScanning(false);
+ }, [mode]);
+
+ return {
+ isScanning,
+ startScan,
+ stopScan,
+ };
+};
+
diff --git a/context/UserContext.tsx b/context/UserContext.tsx
index b9f4fdc..e80bbf0 100644
--- a/context/UserContext.tsx
+++ b/context/UserContext.tsx
@@ -117,7 +117,7 @@ export const UserProvider: React.FC<{children: React.ReactNode}> = ({children})
try {
// Load address type
const storedType = (await LocalCache.getItem('addressType')) as AddressType | null;
- const currentAddressType = (storedType as AddressType) || 'legacy';
+ const currentAddressType = (storedType as AddressType) || 'segwit-native';
setActiveAddressTypeState(currentAddressType);
// Load/derive btcPub
diff --git a/context/WalletContext.tsx b/context/WalletContext.tsx
index fa93818..a46cc37 100644
--- a/context/WalletContext.tsx
+++ b/context/WalletContext.tsx
@@ -59,7 +59,7 @@ export const WalletProvider: React.FC<{children: React.ReactNode}> = ({
// Get current address type for path calculation
const storedAddressType = await LocalCache.getItem('addressType');
- const currentAddressType = (storedAddressType as string) || 'legacy';
+ const currentAddressType = (storedAddressType as string) || 'segwit-native';
// Check if this is a legacy wallet (created before migration timestamp)
const useLegacyPath = isLegacyWallet(ks.created_at);
const path = getDerivePathForNetwork(net, currentAddressType, useLegacyPath);
diff --git a/ios/BoldWallet.xcodeproj/project.pbxproj b/ios/BoldWallet.xcodeproj/project.pbxproj
index 2bd2b1e..d7fcda3 100644
--- a/ios/BoldWallet.xcodeproj/project.pbxproj
+++ b/ios/BoldWallet.xcodeproj/project.pbxproj
@@ -518,7 +518,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;
CLANG_ENABLE_MODULES = YES;
- CURRENT_PROJECT_VERSION = 35;
+ CURRENT_PROJECT_VERSION = 37;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 2G529K765N;
ENABLE_BITCODE = NO;
@@ -532,7 +532,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
- MARKETING_VERSION = 2.1.2;
+ MARKETING_VERSION = 2.1.4;
ONLY_ACTIVE_ARCH = NO;
OTHER_LDFLAGS = (
"$(inherited)",
@@ -556,7 +556,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = NO;
CLANG_ENABLE_MODULES = YES;
- CURRENT_PROJECT_VERSION = 35;
+ CURRENT_PROJECT_VERSION = 37;
DEAD_CODE_STRIPPING = YES;
DEVELOPMENT_TEAM = 2G529K765N;
ENABLE_TESTABILITY = NO;
@@ -569,7 +569,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
- MARKETING_VERSION = 2.1.2;
+ MARKETING_VERSION = 2.1.4;
ONLY_ACTIVE_ARCH = YES;
OTHER_LDFLAGS = (
"$(inherited)",
diff --git a/ios/Tss.xcframework/Info.plist b/ios/Tss.xcframework/Info.plist
index c81fd9d..6ff0575 100644
--- a/ios/Tss.xcframework/Info.plist
+++ b/ios/Tss.xcframework/Info.plist
@@ -8,21 +8,24 @@
BinaryPath
Tss.framework/Tss
LibraryIdentifier
- ios-arm64
+ ios-arm64_x86_64-simulator
LibraryPath
Tss.framework
SupportedArchitectures
arm64
+ x86_64
SupportedPlatform
ios
+ SupportedPlatformVariant
+ simulator
BinaryPath
- Tss.framework/Tss
+ Tss.framework/Versions/A/Tss
LibraryIdentifier
- ios-arm64_x86_64-simulator
+ macos-arm64_x86_64
LibraryPath
Tss.framework
SupportedArchitectures
@@ -31,24 +34,21 @@
x86_64
SupportedPlatform
- ios
- SupportedPlatformVariant
- simulator
+ macos
BinaryPath
- Tss.framework/Versions/A/Tss
+ Tss.framework/Tss
LibraryIdentifier
- macos-arm64_x86_64
+ ios-arm64
LibraryPath
Tss.framework
SupportedArchitectures
arm64
- x86_64
SupportedPlatform
- macos
+ ios
CFBundlePackageType
diff --git a/ios/Tss.xcframework/ios-arm64/Tss.framework/Headers/Tss.objc.h b/ios/Tss.xcframework/ios-arm64/Tss.framework/Headers/Tss.objc.h
index 0a50558..723ad0f 100644
--- a/ios/Tss.xcframework/ios-arm64/Tss.framework/Headers/Tss.objc.h
+++ b/ios/Tss.xcframework/ios-arm64/Tss.framework/Headers/Tss.objc.h
@@ -270,6 +270,8 @@
// skipped field UTXO.Vout with unsupported type: uint32
@property (nonatomic) int64_t value;
+// skipped field UTXO.Status with unsupported type: struct{Confirmed bool "json:\"confirmed\""; BlockHeight int64 "json:\"block_height\""}
+
@end
// skipped const MaxUint32 with unsupported type: uint32
diff --git a/ios/Tss.xcframework/ios-arm64/Tss.framework/Info.plist b/ios/Tss.xcframework/ios-arm64/Tss.framework/Info.plist
index c08298e..70b7423 100644
--- a/ios/Tss.xcframework/ios-arm64/Tss.framework/Info.plist
+++ b/ios/Tss.xcframework/ios-arm64/Tss.framework/Info.plist
@@ -9,9 +9,9 @@
MinimumOSVersion
100.0
CFBundleShortVersionString
- 0.0.1765909426
+ 0.0.1767079533
CFBundleVersion
- 0.0.1765909426
+ 0.0.1767079533
CFBundlePackageType
FMWK
diff --git a/ios/Tss.xcframework/ios-arm64/Tss.framework/Tss b/ios/Tss.xcframework/ios-arm64/Tss.framework/Tss
index dade8fb..b6460f3 100644
Binary files a/ios/Tss.xcframework/ios-arm64/Tss.framework/Tss and b/ios/Tss.xcframework/ios-arm64/Tss.framework/Tss differ
diff --git a/ios/Tss.xcframework/ios-arm64_x86_64-simulator/Tss.framework/Headers/Tss.objc.h b/ios/Tss.xcframework/ios-arm64_x86_64-simulator/Tss.framework/Headers/Tss.objc.h
index 0a50558..723ad0f 100644
--- a/ios/Tss.xcframework/ios-arm64_x86_64-simulator/Tss.framework/Headers/Tss.objc.h
+++ b/ios/Tss.xcframework/ios-arm64_x86_64-simulator/Tss.framework/Headers/Tss.objc.h
@@ -270,6 +270,8 @@
// skipped field UTXO.Vout with unsupported type: uint32
@property (nonatomic) int64_t value;
+// skipped field UTXO.Status with unsupported type: struct{Confirmed bool "json:\"confirmed\""; BlockHeight int64 "json:\"block_height\""}
+
@end
// skipped const MaxUint32 with unsupported type: uint32
diff --git a/ios/Tss.xcframework/ios-arm64_x86_64-simulator/Tss.framework/Info.plist b/ios/Tss.xcframework/ios-arm64_x86_64-simulator/Tss.framework/Info.plist
index c08298e..70b7423 100644
--- a/ios/Tss.xcframework/ios-arm64_x86_64-simulator/Tss.framework/Info.plist
+++ b/ios/Tss.xcframework/ios-arm64_x86_64-simulator/Tss.framework/Info.plist
@@ -9,9 +9,9 @@
MinimumOSVersion
100.0
CFBundleShortVersionString
- 0.0.1765909426
+ 0.0.1767079533
CFBundleVersion
- 0.0.1765909426
+ 0.0.1767079533
CFBundlePackageType
FMWK
diff --git a/ios/Tss.xcframework/ios-arm64_x86_64-simulator/Tss.framework/Tss b/ios/Tss.xcframework/ios-arm64_x86_64-simulator/Tss.framework/Tss
index 127c3ca..b12aec7 100644
Binary files a/ios/Tss.xcframework/ios-arm64_x86_64-simulator/Tss.framework/Tss and b/ios/Tss.xcframework/ios-arm64_x86_64-simulator/Tss.framework/Tss differ
diff --git a/ios/Tss.xcframework/macos-arm64_x86_64/Tss.framework/Versions/A/Headers/Tss.objc.h b/ios/Tss.xcframework/macos-arm64_x86_64/Tss.framework/Versions/A/Headers/Tss.objc.h
index 0a50558..723ad0f 100644
--- a/ios/Tss.xcframework/macos-arm64_x86_64/Tss.framework/Versions/A/Headers/Tss.objc.h
+++ b/ios/Tss.xcframework/macos-arm64_x86_64/Tss.framework/Versions/A/Headers/Tss.objc.h
@@ -270,6 +270,8 @@
// skipped field UTXO.Vout with unsupported type: uint32
@property (nonatomic) int64_t value;
+// skipped field UTXO.Status with unsupported type: struct{Confirmed bool "json:\"confirmed\""; BlockHeight int64 "json:\"block_height\""}
+
@end
// skipped const MaxUint32 with unsupported type: uint32
diff --git a/ios/Tss.xcframework/macos-arm64_x86_64/Tss.framework/Versions/A/Resources/Info.plist b/ios/Tss.xcframework/macos-arm64_x86_64/Tss.framework/Versions/A/Resources/Info.plist
index c08298e..70b7423 100644
--- a/ios/Tss.xcframework/macos-arm64_x86_64/Tss.framework/Versions/A/Resources/Info.plist
+++ b/ios/Tss.xcframework/macos-arm64_x86_64/Tss.framework/Versions/A/Resources/Info.plist
@@ -9,9 +9,9 @@
MinimumOSVersion
100.0
CFBundleShortVersionString
- 0.0.1765909426
+ 0.0.1767079533
CFBundleVersion
- 0.0.1765909426
+ 0.0.1767079533
CFBundlePackageType
FMWK
diff --git a/ios/Tss.xcframework/macos-arm64_x86_64/Tss.framework/Versions/A/Tss b/ios/Tss.xcframework/macos-arm64_x86_64/Tss.framework/Versions/A/Tss
index 89faaee..1cfbebc 100644
Binary files a/ios/Tss.xcframework/macos-arm64_x86_64/Tss.framework/Versions/A/Tss and b/ios/Tss.xcframework/macos-arm64_x86_64/Tss.framework/Versions/A/Tss differ
diff --git a/package.json b/package.json
index 43b988f..04d064f 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "boldwallet",
- "version": "2.1.2",
+ "version": "2.1.3",
"private": true,
"scripts": {
"android": "react-native run-android",
diff --git a/patches/rn-barcode-zxing-scan+1.0.4.patch b/patches/rn-barcode-zxing-scan+1.0.4.patch
index 39070d4..800b381 100644
--- a/patches/rn-barcode-zxing-scan+1.0.4.patch
+++ b/patches/rn-barcode-zxing-scan+1.0.4.patch
@@ -167,7 +167,7 @@ index 0000000..53a174a
\ No newline at end of file
diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/aapt_friendly_merged_manifests/debug/processDebugManifest/aapt/output-metadata.json b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/aapt_friendly_merged_manifests/debug/processDebugManifest/aapt/output-metadata.json
new file mode 100644
-index 0000000..c03fd85
+index 0000000..99bfe68
--- /dev/null
+++ b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/aapt_friendly_merged_manifests/debug/processDebugManifest/aapt/output-metadata.json
@@ -0,0 +1,18 @@
@@ -178,54 +178,7 @@ index 0000000..c03fd85
+ "kind": "Directory"
+ },
+ "applicationId": "com.reactlibrary",
-+ "variantName": "release",
-+ "elements": [
-+ {
-+ "type": "SINGLE",
-+ "filters": [],
-+ "attributes": [],
-+ "outputFile": "AndroidManifest.xml"
-+ }
-+ ],
-+ "elementType": "File"
-+}
-\ No newline at end of file
-diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/aapt_friendly_merged_manifests/release/processReleaseManifest/aapt/AndroidManifest.xml b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/aapt_friendly_merged_manifests/release/processReleaseManifest/aapt/AndroidManifest.xml
-new file mode 100644
-index 0000000..53a174a
---- /dev/null
-+++ b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/aapt_friendly_merged_manifests/release/processReleaseManifest/aapt/AndroidManifest.xml
-@@ -0,0 +1,15 @@
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-\ No newline at end of file
-diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/aapt_friendly_merged_manifests/release/processReleaseManifest/aapt/output-metadata.json b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/aapt_friendly_merged_manifests/release/processReleaseManifest/aapt/output-metadata.json
-new file mode 100644
-index 0000000..c03fd85
---- /dev/null
-+++ b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/aapt_friendly_merged_manifests/release/processReleaseManifest/aapt/output-metadata.json
-@@ -0,0 +1,18 @@
-+{
-+ "version": 3,
-+ "artifactType": {
-+ "type": "AAPT_FRIENDLY_MERGED_MANIFESTS",
-+ "kind": "Directory"
-+ },
-+ "applicationId": "com.reactlibrary",
-+ "variantName": "release",
++ "variantName": "debug",
+ "elements": [
+ {
+ "type": "SINGLE",
@@ -236,1691 +189,434 @@ index 0000000..c03fd85
+ ],
+ "elementType": "File"
+}
-\ No newline at end of file
-diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/aar_main_jar/release/syncReleaseLibJars/classes.jar b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/aar_main_jar/release/syncReleaseLibJars/classes.jar
-new file mode 100644
-index 0000000..dea64bd
-Binary files /dev/null and b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/aar_main_jar/release/syncReleaseLibJars/classes.jar differ
-diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/aar_metadata/debug/writeDebugAarMetadata/aar-metadata.properties b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/aar_metadata/debug/writeDebugAarMetadata/aar-metadata.properties
-new file mode 100644
-index 0000000..1211b1e
---- /dev/null
-+++ b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/aar_metadata/debug/writeDebugAarMetadata/aar-metadata.properties
-@@ -0,0 +1,6 @@
-+aarFormatVersion=1.0
-+aarMetadataVersion=1.0
-+minCompileSdk=1
-+minCompileSdkExtension=0
-+minAndroidGradlePluginVersion=1.0.0
-+coreLibraryDesugaringEnabled=false
-diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/aar_metadata/release/writeReleaseAarMetadata/aar-metadata.properties b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/aar_metadata/release/writeReleaseAarMetadata/aar-metadata.properties
-new file mode 100644
-index 0000000..1211b1e
---- /dev/null
-+++ b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/aar_metadata/release/writeReleaseAarMetadata/aar-metadata.properties
-@@ -0,0 +1,6 @@
-+aarFormatVersion=1.0
-+aarMetadataVersion=1.0
-+minCompileSdk=1
-+minCompileSdkExtension=0
-+minAndroidGradlePluginVersion=1.0.0
-+coreLibraryDesugaringEnabled=false
-diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/annotation_processor_list/debug/javaPreCompileDebug/annotationProcessors.json b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/annotation_processor_list/debug/javaPreCompileDebug/annotationProcessors.json
-new file mode 100644
-index 0000000..9e26dfe
---- /dev/null
-+++ b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/annotation_processor_list/debug/javaPreCompileDebug/annotationProcessors.json
-@@ -0,0 +1 @@
-+{}
-\ No newline at end of file
-diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/annotation_processor_list/release/javaPreCompileRelease/annotationProcessors.json b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/annotation_processor_list/release/javaPreCompileRelease/annotationProcessors.json
-new file mode 100644
-index 0000000..9e26dfe
---- /dev/null
-+++ b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/annotation_processor_list/release/javaPreCompileRelease/annotationProcessors.json
-@@ -0,0 +1 @@
-+{}
-\ No newline at end of file
-diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/annotations_typedef_file/release/extractReleaseAnnotations/typedefs.txt b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/annotations_typedef_file/release/extractReleaseAnnotations/typedefs.txt
-new file mode 100644
-index 0000000..e69de29
-diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/compile_library_classes_jar/debug/bundleLibCompileToJarDebug/classes.jar b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/compile_library_classes_jar/debug/bundleLibCompileToJarDebug/classes.jar
-new file mode 100644
-index 0000000..7e41b57
-Binary files /dev/null and b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/compile_library_classes_jar/debug/bundleLibCompileToJarDebug/classes.jar differ
-diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/compile_library_classes_jar/release/bundleLibCompileToJarRelease/classes.jar b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/compile_library_classes_jar/release/bundleLibCompileToJarRelease/classes.jar
-new file mode 100644
-index 0000000..15a3ae0
-Binary files /dev/null and b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/compile_library_classes_jar/release/bundleLibCompileToJarRelease/classes.jar differ
-diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/compile_r_class_jar/debug/generateDebugRFile/R.jar b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/compile_r_class_jar/debug/generateDebugRFile/R.jar
-new file mode 100644
-index 0000000..89780f8
-Binary files /dev/null and b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/compile_r_class_jar/debug/generateDebugRFile/R.jar differ
-diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/compile_r_class_jar/release/generateReleaseRFile/R.jar b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/compile_r_class_jar/release/generateReleaseRFile/R.jar
-new file mode 100644
-index 0000000..89780f8
-Binary files /dev/null and b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/compile_r_class_jar/release/generateReleaseRFile/R.jar differ
-diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/compile_symbol_list/debug/generateDebugRFile/R.txt b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/compile_symbol_list/debug/generateDebugRFile/R.txt
-new file mode 100644
-index 0000000..e69de29
-diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/compile_symbol_list/release/generateReleaseRFile/R.txt b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/compile_symbol_list/release/generateReleaseRFile/R.txt
-new file mode 100644
-index 0000000..e69de29
-diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/default_proguard_files/global/proguard-android-optimize.txt-8.12.0 b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/default_proguard_files/global/proguard-android-optimize.txt-8.12.0
-new file mode 100644
-index 0000000..5a3e3a5
---- /dev/null
-+++ b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/default_proguard_files/global/proguard-android-optimize.txt-8.12.0
-@@ -0,0 +1,89 @@
-+# This is a configuration file for ProGuard.
-+# http://proguard.sourceforge.net/index.html#manual/usage.html
-+#
-+# Starting with version 2.2 of the Android plugin for Gradle, this file is distributed together with
-+# the plugin and unpacked at build-time. The files in $ANDROID_HOME are no longer maintained and
-+# will be ignored by new version of the Android plugin for Gradle.
-+
-+# Optimizations: If you don't want to optimize, use the proguard-android.txt configuration file
-+# instead of this one, which turns off the optimization flags.
-+-allowaccessmodification
-+
-+# Preserve some attributes that may be required for reflection.
-+-keepattributes AnnotationDefault,
-+ EnclosingMethod,
-+ InnerClasses,
-+ RuntimeVisibleAnnotations,
-+ RuntimeVisibleParameterAnnotations,
-+ RuntimeVisibleTypeAnnotations,
-+ Signature
-+
-+-keep public class com.google.vending.licensing.ILicensingService
-+-keep public class com.android.vending.licensing.ILicensingService
-+-keep public class com.google.android.vending.licensing.ILicensingService
-+-dontnote com.android.vending.licensing.ILicensingService
-+-dontnote com.google.vending.licensing.ILicensingService
-+-dontnote com.google.android.vending.licensing.ILicensingService
-+
-+# For native methods, see https://www.guardsquare.com/manual/configuration/examples#native
-+-keepclasseswithmembernames,includedescriptorclasses class * {
-+ native ;
-+}
-+
-+# Keep setters in Views so that animations can still work.
-+-keepclassmembers public class * extends android.view.View {
-+ void set*(***);
-+ *** get*();
-+}
-+
-+# We want to keep methods in Activity that could be used in the XML attribute onClick.
-+-keepclassmembers class * extends android.app.Activity {
-+ public void *(android.view.View);
-+}
-+
-+# For enumeration classes, see https://www.guardsquare.com/manual/configuration/examples#enumerations
-+-keepclassmembers enum * {
-+ public static **[] values();
-+ public static ** valueOf(java.lang.String);
-+}
-+
-+-keepclassmembers class * implements android.os.Parcelable {
-+ public static final ** CREATOR;
-+}
-+
-+# Preserve annotated Javascript interface methods.
-+-keepclassmembers class * {
-+ @android.webkit.JavascriptInterface ;
-+}
-+
-+# The support libraries contains references to newer platform versions.
-+# Don't warn about those in case this app is linking against an older
-+# platform version. We know about them, and they are safe.
-+-dontnote android.support.**
-+-dontnote androidx.**
-+-dontwarn android.support.**
-+-dontwarn androidx.**
-+
-+# Understand the @Keep support annotation.
-+-keep class android.support.annotation.Keep
-+
-+-keep @android.support.annotation.Keep class * {*;}
-+
-+-keepclasseswithmembers class * {
-+ @android.support.annotation.Keep ;
-+}
-+
-+-keepclasseswithmembers class * {
-+ @android.support.annotation.Keep ;
-+}
-+
-+-keepclasseswithmembers class * {
-+ @android.support.annotation.Keep (...);
-+}
-+
-+# These classes are duplicated between android.jar and org.apache.http.legacy.jar.
-+-dontnote org.apache.http.**
-+-dontnote android.net.http.**
-+
-+# These classes are duplicated between android.jar and core-lambda-stubs.jar.
-+-dontnote java.lang.invoke.**
-diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/default_proguard_files/global/proguard-android.txt-8.12.0 b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/default_proguard_files/global/proguard-android.txt-8.12.0
-new file mode 100644
-index 0000000..6f7e4ef
---- /dev/null
-+++ b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/default_proguard_files/global/proguard-android.txt-8.12.0
-@@ -0,0 +1,95 @@
-+# This is a configuration file for ProGuard.
-+# http://proguard.sourceforge.net/index.html#manual/usage.html
-+#
-+# Starting with version 2.2 of the Android plugin for Gradle, this file is distributed together with
-+# the plugin and unpacked at build-time. The files in $ANDROID_HOME are no longer maintained and
-+# will be ignored by new version of the Android plugin for Gradle.
-+
-+# Optimization is turned off by default. Dex does not like code run
-+# through the ProGuard optimize steps (and performs some
-+# of these optimizations on its own).
-+# Note that if you want to enable optimization, you cannot just
-+# include optimization flags in your own project configuration file;
-+# instead you will need to point to the
-+# "proguard-android-optimize.txt" file instead of this one from your
-+# project.properties file.
-+-dontoptimize
-+
-+# Preserve some attributes that may be required for reflection.
-+-keepattributes AnnotationDefault,
-+ EnclosingMethod,
-+ InnerClasses,
-+ RuntimeVisibleAnnotations,
-+ RuntimeVisibleParameterAnnotations,
-+ RuntimeVisibleTypeAnnotations,
-+ Signature
-+
-+-keep public class com.google.vending.licensing.ILicensingService
-+-keep public class com.android.vending.licensing.ILicensingService
-+-keep public class com.google.android.vending.licensing.ILicensingService
-+-dontnote com.android.vending.licensing.ILicensingService
-+-dontnote com.google.vending.licensing.ILicensingService
-+-dontnote com.google.android.vending.licensing.ILicensingService
-+
-+# For native methods, see https://www.guardsquare.com/manual/configuration/examples#native
-+-keepclasseswithmembernames,includedescriptorclasses class * {
-+ native ;
-+}
-+
-+# Keep setters in Views so that animations can still work.
-+-keepclassmembers public class * extends android.view.View {
-+ void set*(***);
-+ *** get*();
-+}
-+
-+# We want to keep methods in Activity that could be used in the XML attribute onClick.
-+-keepclassmembers class * extends android.app.Activity {
-+ public void *(android.view.View);
-+}
-+
-+# For enumeration classes, see https://www.guardsquare.com/manual/configuration/examples#enumerations
-+-keepclassmembers enum * {
-+ public static **[] values();
-+ public static ** valueOf(java.lang.String);
-+}
-+
-+-keepclassmembers class * implements android.os.Parcelable {
-+ public static final ** CREATOR;
-+}
-+
-+# Preserve annotated Javascript interface methods.
-+-keepclassmembers class * {
-+ @android.webkit.JavascriptInterface ;
-+}
-+
-+# The support libraries contains references to newer platform versions.
-+# Don't warn about those in case this app is linking against an older
-+# platform version. We know about them, and they are safe.
-+-dontnote android.support.**
-+-dontnote androidx.**
-+-dontwarn android.support.**
-+-dontwarn androidx.**
-+
-+# Understand the @Keep support annotation.
-+-keep class android.support.annotation.Keep
-+
-+-keep @android.support.annotation.Keep class * {*;}
-+
-+-keepclasseswithmembers class * {
-+ @android.support.annotation.Keep ;
-+}
-+
-+-keepclasseswithmembers class * {
-+ @android.support.annotation.Keep ;
-+}
-+
-+-keepclasseswithmembers class * {
-+ @android.support.annotation.Keep (...);
-+}
-+
-+# These classes are duplicated between android.jar and org.apache.http.legacy.jar.
-+-dontnote org.apache.http.**
-+-dontnote android.net.http.**
-+
-+# These classes are duplicated between android.jar and core-lambda-stubs.jar.
-+-dontnote java.lang.invoke.**
-diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/default_proguard_files/global/proguard-defaults.txt-8.12.0 b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/default_proguard_files/global/proguard-defaults.txt-8.12.0
-new file mode 100644
-index 0000000..7bbb228
---- /dev/null
-+++ b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/default_proguard_files/global/proguard-defaults.txt-8.12.0
-@@ -0,0 +1,89 @@
-+# This is a configuration file for ProGuard.
-+# http://proguard.sourceforge.net/index.html#manual/usage.html
-+#
-+# Starting with version 2.2 of the Android plugin for Gradle, this file is distributed together with
-+# the plugin and unpacked at build-time. The files in $ANDROID_HOME are no longer maintained and
-+# will be ignored by new version of the Android plugin for Gradle.
-+
-+# Optimizations can be turned on and off in the 'postProcessing' DSL block.
-+# The configuration below is applied if optimizations are enabled.
-+-allowaccessmodification
-+
-+# Preserve some attributes that may be required for reflection.
-+-keepattributes AnnotationDefault,
-+ EnclosingMethod,
-+ InnerClasses,
-+ RuntimeVisibleAnnotations,
-+ RuntimeVisibleParameterAnnotations,
-+ RuntimeVisibleTypeAnnotations,
-+ Signature
-+
-+-keep public class com.google.vending.licensing.ILicensingService
-+-keep public class com.android.vending.licensing.ILicensingService
-+-keep public class com.google.android.vending.licensing.ILicensingService
-+-dontnote com.android.vending.licensing.ILicensingService
-+-dontnote com.google.vending.licensing.ILicensingService
-+-dontnote com.google.android.vending.licensing.ILicensingService
-+
-+# For native methods, see https://www.guardsquare.com/manual/configuration/examples#native
-+-keepclasseswithmembernames,includedescriptorclasses class * {
-+ native ;
-+}
-+
-+# Keep setters in Views so that animations can still work.
-+-keepclassmembers public class * extends android.view.View {
-+ void set*(***);
-+ *** get*();
-+}
-+
-+# We want to keep methods in Activity that could be used in the XML attribute onClick.
-+-keepclassmembers class * extends android.app.Activity {
-+ public void *(android.view.View);
-+}
-+
-+# For enumeration classes, see https://www.guardsquare.com/manual/configuration/examples#enumerations
-+-keepclassmembers enum * {
-+ public static **[] values();
-+ public static ** valueOf(java.lang.String);
-+}
-+
-+-keepclassmembers class * implements android.os.Parcelable {
-+ public static final ** CREATOR;
-+}
-+
-+# Preserve annotated Javascript interface methods.
-+-keepclassmembers class * {
-+ @android.webkit.JavascriptInterface ;
-+}
-+
-+# The support libraries contains references to newer platform versions.
-+# Don't warn about those in case this app is linking against an older
-+# platform version. We know about them, and they are safe.
-+-dontnote android.support.**
-+-dontnote androidx.**
-+-dontwarn android.support.**
-+-dontwarn androidx.**
-+
-+# Understand the @Keep support annotation.
-+-keep class android.support.annotation.Keep
-+
-+-keep @android.support.annotation.Keep class * {*;}
-+
-+-keepclasseswithmembers class * {
-+ @android.support.annotation.Keep ;
-+}
-+
-+-keepclasseswithmembers class * {
-+ @android.support.annotation.Keep ;
-+}
-+
-+-keepclasseswithmembers class * {
-+ @android.support.annotation.Keep (...);
-+}
-+
-+# These classes are duplicated between android.jar and org.apache.http.legacy.jar.
-+-dontnote org.apache.http.**
-+-dontnote android.net.http.**
-+
-+# These classes are duplicated between android.jar and core-lambda-stubs.jar.
-+-dontnote java.lang.invoke.**
-diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/full_jar/release/createFullJarRelease/full.jar b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/full_jar/release/createFullJarRelease/full.jar
-new file mode 100644
-index 0000000..86eed0b
-Binary files /dev/null and b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/full_jar/release/createFullJarRelease/full.jar differ
-diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/incremental/debug/packageDebugResources/compile-file-map.properties b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/incremental/debug/packageDebugResources/compile-file-map.properties
-new file mode 100644
-index 0000000..02d5733
---- /dev/null
-+++ b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/incremental/debug/packageDebugResources/compile-file-map.properties
-@@ -0,0 +1 @@
-+#Mon Dec 08 17:57:07 GST 2025
-diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/incremental/debug/packageDebugResources/merger.xml b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/incremental/debug/packageDebugResources/merger.xml
-new file mode 100644
-index 0000000..9ac068c
---- /dev/null
-+++ b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/incremental/debug/packageDebugResources/merger.xml
-@@ -0,0 +1,2 @@
-+
-+
-\ No newline at end of file
-diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/incremental/lintVitalAnalyzeRelease/module.xml b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/incremental/lintVitalAnalyzeRelease/module.xml
-new file mode 100644
-index 0000000..385fb17
---- /dev/null
-+++ b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/incremental/lintVitalAnalyzeRelease/module.xml
-@@ -0,0 +1,18 @@
-+
-+
-+
-+
-diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/incremental/lintVitalAnalyzeRelease/release-artifact-dependencies.xml b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/incremental/lintVitalAnalyzeRelease/release-artifact-dependencies.xml
-new file mode 100644
-index 0000000..b01967a
---- /dev/null
-+++ b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/incremental/lintVitalAnalyzeRelease/release-artifact-dependencies.xml
-@@ -0,0 +1,440 @@
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/incremental/lintVitalAnalyzeRelease/release-artifact-libraries.xml b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/incremental/lintVitalAnalyzeRelease/release-artifact-libraries.xml
-new file mode 100644
-index 0000000..fa93e7f
---- /dev/null
-+++ b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/incremental/lintVitalAnalyzeRelease/release-artifact-libraries.xml
-@@ -0,0 +1,791 @@
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/incremental/lintVitalAnalyzeRelease/release.xml b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/incremental/lintVitalAnalyzeRelease/release.xml
+\ No newline at end of file
+diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/aapt_friendly_merged_manifests/release/processReleaseManifest/aapt/AndroidManifest.xml b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/aapt_friendly_merged_manifests/release/processReleaseManifest/aapt/AndroidManifest.xml
new file mode 100644
-index 0000000..a69c168
+index 0000000..53a174a
--- /dev/null
-+++ b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/incremental/lintVitalAnalyzeRelease/release.xml
-@@ -0,0 +1,31 @@
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
++++ b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/aapt_friendly_merged_manifests/release/processReleaseManifest/aapt/AndroidManifest.xml
+@@ -0,0 +1,15 @@
++
++
++
++
++
++
++
++
++
++
++
+\ No newline at end of file
+diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/aapt_friendly_merged_manifests/release/processReleaseManifest/aapt/output-metadata.json b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/aapt_friendly_merged_manifests/release/processReleaseManifest/aapt/output-metadata.json
+new file mode 100644
+index 0000000..c03fd85
+--- /dev/null
++++ b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/aapt_friendly_merged_manifests/release/processReleaseManifest/aapt/output-metadata.json
+@@ -0,0 +1,18 @@
++{
++ "version": 3,
++ "artifactType": {
++ "type": "AAPT_FRIENDLY_MERGED_MANIFESTS",
++ "kind": "Directory"
++ },
++ "applicationId": "com.reactlibrary",
++ "variantName": "release",
++ "elements": [
++ {
++ "type": "SINGLE",
++ "filters": [],
++ "attributes": [],
++ "outputFile": "AndroidManifest.xml"
++ }
++ ],
++ "elementType": "File"
++}
+\ No newline at end of file
+diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/aar_main_jar/release/syncReleaseLibJars/classes.jar b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/aar_main_jar/release/syncReleaseLibJars/classes.jar
+new file mode 100644
+index 0000000..dea64bd
+Binary files /dev/null and b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/aar_main_jar/release/syncReleaseLibJars/classes.jar differ
+diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/aar_metadata/debug/writeDebugAarMetadata/aar-metadata.properties b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/aar_metadata/debug/writeDebugAarMetadata/aar-metadata.properties
+new file mode 100644
+index 0000000..1211b1e
+--- /dev/null
++++ b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/aar_metadata/debug/writeDebugAarMetadata/aar-metadata.properties
+@@ -0,0 +1,6 @@
++aarFormatVersion=1.0
++aarMetadataVersion=1.0
++minCompileSdk=1
++minCompileSdkExtension=0
++minAndroidGradlePluginVersion=1.0.0
++coreLibraryDesugaringEnabled=false
+diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/aar_metadata/release/writeReleaseAarMetadata/aar-metadata.properties b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/aar_metadata/release/writeReleaseAarMetadata/aar-metadata.properties
+new file mode 100644
+index 0000000..1211b1e
+--- /dev/null
++++ b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/aar_metadata/release/writeReleaseAarMetadata/aar-metadata.properties
+@@ -0,0 +1,6 @@
++aarFormatVersion=1.0
++aarMetadataVersion=1.0
++minCompileSdk=1
++minCompileSdkExtension=0
++minAndroidGradlePluginVersion=1.0.0
++coreLibraryDesugaringEnabled=false
+diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/annotation_processor_list/debug/javaPreCompileDebug/annotationProcessors.json b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/annotation_processor_list/debug/javaPreCompileDebug/annotationProcessors.json
+new file mode 100644
+index 0000000..9e26dfe
+--- /dev/null
++++ b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/annotation_processor_list/debug/javaPreCompileDebug/annotationProcessors.json
+@@ -0,0 +1 @@
++{}
+\ No newline at end of file
+diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/annotation_processor_list/release/javaPreCompileRelease/annotationProcessors.json b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/annotation_processor_list/release/javaPreCompileRelease/annotationProcessors.json
+new file mode 100644
+index 0000000..9e26dfe
+--- /dev/null
++++ b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/annotation_processor_list/release/javaPreCompileRelease/annotationProcessors.json
+@@ -0,0 +1 @@
++{}
+\ No newline at end of file
+diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/annotations_typedef_file/release/extractReleaseAnnotations/typedefs.txt b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/annotations_typedef_file/release/extractReleaseAnnotations/typedefs.txt
+new file mode 100644
+index 0000000..e69de29
+diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/compile_library_classes_jar/debug/bundleLibCompileToJarDebug/classes.jar b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/compile_library_classes_jar/debug/bundleLibCompileToJarDebug/classes.jar
+new file mode 100644
+index 0000000..7e41b57
+Binary files /dev/null and b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/compile_library_classes_jar/debug/bundleLibCompileToJarDebug/classes.jar differ
+diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/compile_library_classes_jar/release/bundleLibCompileToJarRelease/classes.jar b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/compile_library_classes_jar/release/bundleLibCompileToJarRelease/classes.jar
+new file mode 100644
+index 0000000..15a3ae0
+Binary files /dev/null and b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/compile_library_classes_jar/release/bundleLibCompileToJarRelease/classes.jar differ
+diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/compile_r_class_jar/debug/generateDebugRFile/R.jar b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/compile_r_class_jar/debug/generateDebugRFile/R.jar
+new file mode 100644
+index 0000000..89780f8
+Binary files /dev/null and b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/compile_r_class_jar/debug/generateDebugRFile/R.jar differ
+diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/compile_r_class_jar/release/generateReleaseRFile/R.jar b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/compile_r_class_jar/release/generateReleaseRFile/R.jar
+new file mode 100644
+index 0000000..89780f8
+Binary files /dev/null and b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/compile_r_class_jar/release/generateReleaseRFile/R.jar differ
+diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/compile_symbol_list/debug/generateDebugRFile/R.txt b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/compile_symbol_list/debug/generateDebugRFile/R.txt
+new file mode 100644
+index 0000000..e69de29
+diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/compile_symbol_list/release/generateReleaseRFile/R.txt b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/compile_symbol_list/release/generateReleaseRFile/R.txt
+new file mode 100644
+index 0000000..e69de29
+diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/default_proguard_files/global/proguard-android-optimize.txt-8.12.0 b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/default_proguard_files/global/proguard-android-optimize.txt-8.12.0
+new file mode 100644
+index 0000000..5a3e3a5
+--- /dev/null
++++ b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/default_proguard_files/global/proguard-android-optimize.txt-8.12.0
+@@ -0,0 +1,89 @@
++# This is a configuration file for ProGuard.
++# http://proguard.sourceforge.net/index.html#manual/usage.html
++#
++# Starting with version 2.2 of the Android plugin for Gradle, this file is distributed together with
++# the plugin and unpacked at build-time. The files in $ANDROID_HOME are no longer maintained and
++# will be ignored by new version of the Android plugin for Gradle.
++
++# Optimizations: If you don't want to optimize, use the proguard-android.txt configuration file
++# instead of this one, which turns off the optimization flags.
++-allowaccessmodification
++
++# Preserve some attributes that may be required for reflection.
++-keepattributes AnnotationDefault,
++ EnclosingMethod,
++ InnerClasses,
++ RuntimeVisibleAnnotations,
++ RuntimeVisibleParameterAnnotations,
++ RuntimeVisibleTypeAnnotations,
++ Signature
++
++-keep public class com.google.vending.licensing.ILicensingService
++-keep public class com.android.vending.licensing.ILicensingService
++-keep public class com.google.android.vending.licensing.ILicensingService
++-dontnote com.android.vending.licensing.ILicensingService
++-dontnote com.google.vending.licensing.ILicensingService
++-dontnote com.google.android.vending.licensing.ILicensingService
++
++# For native methods, see https://www.guardsquare.com/manual/configuration/examples#native
++-keepclasseswithmembernames,includedescriptorclasses class * {
++ native ;
++}
++
++# Keep setters in Views so that animations can still work.
++-keepclassmembers public class * extends android.view.View {
++ void set*(***);
++ *** get*();
++}
++
++# We want to keep methods in Activity that could be used in the XML attribute onClick.
++-keepclassmembers class * extends android.app.Activity {
++ public void *(android.view.View);
++}
++
++# For enumeration classes, see https://www.guardsquare.com/manual/configuration/examples#enumerations
++-keepclassmembers enum * {
++ public static **[] values();
++ public static ** valueOf(java.lang.String);
++}
++
++-keepclassmembers class * implements android.os.Parcelable {
++ public static final ** CREATOR;
++}
++
++# Preserve annotated Javascript interface methods.
++-keepclassmembers class * {
++ @android.webkit.JavascriptInterface ;
++}
++
++# The support libraries contains references to newer platform versions.
++# Don't warn about those in case this app is linking against an older
++# platform version. We know about them, and they are safe.
++-dontnote android.support.**
++-dontnote androidx.**
++-dontwarn android.support.**
++-dontwarn androidx.**
++
++# Understand the @Keep support annotation.
++-keep class android.support.annotation.Keep
++
++-keep @android.support.annotation.Keep class * {*;}
++
++-keepclasseswithmembers class * {
++ @android.support.annotation.Keep ;
++}
++
++-keepclasseswithmembers class * {
++ @android.support.annotation.Keep ;
++}
++
++-keepclasseswithmembers class * {
++ @android.support.annotation.Keep (...);
++}
++
++# These classes are duplicated between android.jar and org.apache.http.legacy.jar.
++-dontnote org.apache.http.**
++-dontnote android.net.http.**
++
++# These classes are duplicated between android.jar and core-lambda-stubs.jar.
++-dontnote java.lang.invoke.**
+diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/default_proguard_files/global/proguard-android.txt-8.12.0 b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/default_proguard_files/global/proguard-android.txt-8.12.0
+new file mode 100644
+index 0000000..6f7e4ef
+--- /dev/null
++++ b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/default_proguard_files/global/proguard-android.txt-8.12.0
+@@ -0,0 +1,95 @@
++# This is a configuration file for ProGuard.
++# http://proguard.sourceforge.net/index.html#manual/usage.html
++#
++# Starting with version 2.2 of the Android plugin for Gradle, this file is distributed together with
++# the plugin and unpacked at build-time. The files in $ANDROID_HOME are no longer maintained and
++# will be ignored by new version of the Android plugin for Gradle.
++
++# Optimization is turned off by default. Dex does not like code run
++# through the ProGuard optimize steps (and performs some
++# of these optimizations on its own).
++# Note that if you want to enable optimization, you cannot just
++# include optimization flags in your own project configuration file;
++# instead you will need to point to the
++# "proguard-android-optimize.txt" file instead of this one from your
++# project.properties file.
++-dontoptimize
++
++# Preserve some attributes that may be required for reflection.
++-keepattributes AnnotationDefault,
++ EnclosingMethod,
++ InnerClasses,
++ RuntimeVisibleAnnotations,
++ RuntimeVisibleParameterAnnotations,
++ RuntimeVisibleTypeAnnotations,
++ Signature
++
++-keep public class com.google.vending.licensing.ILicensingService
++-keep public class com.android.vending.licensing.ILicensingService
++-keep public class com.google.android.vending.licensing.ILicensingService
++-dontnote com.android.vending.licensing.ILicensingService
++-dontnote com.google.vending.licensing.ILicensingService
++-dontnote com.google.android.vending.licensing.ILicensingService
++
++# For native methods, see https://www.guardsquare.com/manual/configuration/examples#native
++-keepclasseswithmembernames,includedescriptorclasses class * {
++ native ;
++}
++
++# Keep setters in Views so that animations can still work.
++-keepclassmembers public class * extends android.view.View {
++ void set*(***);
++ *** get*();
++}
++
++# We want to keep methods in Activity that could be used in the XML attribute onClick.
++-keepclassmembers class * extends android.app.Activity {
++ public void *(android.view.View);
++}
++
++# For enumeration classes, see https://www.guardsquare.com/manual/configuration/examples#enumerations
++-keepclassmembers enum * {
++ public static **[] values();
++ public static ** valueOf(java.lang.String);
++}
++
++-keepclassmembers class * implements android.os.Parcelable {
++ public static final ** CREATOR;
++}
++
++# Preserve annotated Javascript interface methods.
++-keepclassmembers class * {
++ @android.webkit.JavascriptInterface ;
++}
++
++# The support libraries contains references to newer platform versions.
++# Don't warn about those in case this app is linking against an older
++# platform version. We know about them, and they are safe.
++-dontnote android.support.**
++-dontnote androidx.**
++-dontwarn android.support.**
++-dontwarn androidx.**
++
++# Understand the @Keep support annotation.
++-keep class android.support.annotation.Keep
++
++-keep @android.support.annotation.Keep class * {*;}
++
++-keepclasseswithmembers class * {
++ @android.support.annotation.Keep ;
++}
++
++-keepclasseswithmembers class * {
++ @android.support.annotation.Keep ;
++}
++
++-keepclasseswithmembers class * {
++ @android.support.annotation.Keep (...);
++}
++
++# These classes are duplicated between android.jar and org.apache.http.legacy.jar.
++-dontnote org.apache.http.**
++-dontnote android.net.http.**
++
++# These classes are duplicated between android.jar and core-lambda-stubs.jar.
++-dontnote java.lang.invoke.**
+diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/default_proguard_files/global/proguard-defaults.txt-8.12.0 b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/default_proguard_files/global/proguard-defaults.txt-8.12.0
+new file mode 100644
+index 0000000..7bbb228
+--- /dev/null
++++ b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/default_proguard_files/global/proguard-defaults.txt-8.12.0
+@@ -0,0 +1,89 @@
++# This is a configuration file for ProGuard.
++# http://proguard.sourceforge.net/index.html#manual/usage.html
++#
++# Starting with version 2.2 of the Android plugin for Gradle, this file is distributed together with
++# the plugin and unpacked at build-time. The files in $ANDROID_HOME are no longer maintained and
++# will be ignored by new version of the Android plugin for Gradle.
++
++# Optimizations can be turned on and off in the 'postProcessing' DSL block.
++# The configuration below is applied if optimizations are enabled.
++-allowaccessmodification
++
++# Preserve some attributes that may be required for reflection.
++-keepattributes AnnotationDefault,
++ EnclosingMethod,
++ InnerClasses,
++ RuntimeVisibleAnnotations,
++ RuntimeVisibleParameterAnnotations,
++ RuntimeVisibleTypeAnnotations,
++ Signature
++
++-keep public class com.google.vending.licensing.ILicensingService
++-keep public class com.android.vending.licensing.ILicensingService
++-keep public class com.google.android.vending.licensing.ILicensingService
++-dontnote com.android.vending.licensing.ILicensingService
++-dontnote com.google.vending.licensing.ILicensingService
++-dontnote com.google.android.vending.licensing.ILicensingService
++
++# For native methods, see https://www.guardsquare.com/manual/configuration/examples#native
++-keepclasseswithmembernames,includedescriptorclasses class * {
++ native ;
++}
++
++# Keep setters in Views so that animations can still work.
++-keepclassmembers public class * extends android.view.View {
++ void set*(***);
++ *** get*();
++}
++
++# We want to keep methods in Activity that could be used in the XML attribute onClick.
++-keepclassmembers class * extends android.app.Activity {
++ public void *(android.view.View);
++}
++
++# For enumeration classes, see https://www.guardsquare.com/manual/configuration/examples#enumerations
++-keepclassmembers enum * {
++ public static **[] values();
++ public static ** valueOf(java.lang.String);
++}
++
++-keepclassmembers class * implements android.os.Parcelable {
++ public static final ** CREATOR;
++}
++
++# Preserve annotated Javascript interface methods.
++-keepclassmembers class * {
++ @android.webkit.JavascriptInterface ;
++}
++
++# The support libraries contains references to newer platform versions.
++# Don't warn about those in case this app is linking against an older
++# platform version. We know about them, and they are safe.
++-dontnote android.support.**
++-dontnote androidx.**
++-dontwarn android.support.**
++-dontwarn androidx.**
++
++# Understand the @Keep support annotation.
++-keep class android.support.annotation.Keep
++
++-keep @android.support.annotation.Keep class * {*;}
++
++-keepclasseswithmembers class * {
++ @android.support.annotation.Keep ;
++}
++
++-keepclasseswithmembers class * {
++ @android.support.annotation.Keep ;
++}
++
++-keepclasseswithmembers class * {
++ @android.support.annotation.Keep (...);
++}
++
++# These classes are duplicated between android.jar and org.apache.http.legacy.jar.
++-dontnote org.apache.http.**
++-dontnote android.net.http.**
++
++# These classes are duplicated between android.jar and core-lambda-stubs.jar.
++-dontnote java.lang.invoke.**
+diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/full_jar/release/createFullJarRelease/full.jar b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/full_jar/release/createFullJarRelease/full.jar
+new file mode 100644
+index 0000000..86eed0b
+Binary files /dev/null and b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/full_jar/release/createFullJarRelease/full.jar differ
+diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/incremental/debug/packageDebugResources/compile-file-map.properties b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/incremental/debug/packageDebugResources/compile-file-map.properties
+new file mode 100644
+index 0000000..8822d04
+--- /dev/null
++++ b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/incremental/debug/packageDebugResources/compile-file-map.properties
+@@ -0,0 +1 @@
++#Mon Dec 29 15:53:58 EET 2025
+diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/incremental/debug/packageDebugResources/merger.xml b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/incremental/debug/packageDebugResources/merger.xml
+new file mode 100644
+index 0000000..9ac068c
+--- /dev/null
++++ b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/incremental/debug/packageDebugResources/merger.xml
+@@ -0,0 +1,2 @@
++
++
+\ No newline at end of file
diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/incremental/mergeDebugAssets/merger.xml b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/incremental/mergeDebugAssets/merger.xml
new file mode 100644
index 0000000..8168c4d
@@ -16841,376 +15537,6 @@ diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/java
new file mode 100644
index 0000000..a140876
Binary files /dev/null and b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/javac/release/compileReleaseJavaWithJavac/classes/com/reactlibrary/ContinuousCaptureActivity.class differ
-diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/lint-cache/lintVitalAnalyzeRelease/lint-cache-version.txt b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/lint-cache/lintVitalAnalyzeRelease/lint-cache-version.txt
-new file mode 100644
-index 0000000..b6ac95c
---- /dev/null
-+++ b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/lint-cache/lintVitalAnalyzeRelease/lint-cache-version.txt
-@@ -0,0 +1 @@
-+Cache for Android Lint31.12.0
-diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/lint-cache/lintVitalAnalyzeRelease/maven.google/com/android/tools/build/group-index.xml b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/lint-cache/lintVitalAnalyzeRelease/maven.google/com/android/tools/build/group-index.xml
-new file mode 100644
-index 0000000..fcd42f5
---- /dev/null
-+++ b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/lint-cache/lintVitalAnalyzeRelease/maven.google/com/android/tools/build/group-index.xml
-@@ -0,0 +1,22 @@
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/lint-cache/lintVitalAnalyzeRelease/maven.google/master-index.xml b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/lint-cache/lintVitalAnalyzeRelease/maven.google/master-index.xml
-new file mode 100644
-index 0000000..cc5b531
---- /dev/null
-+++ b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/lint-cache/lintVitalAnalyzeRelease/maven.google/master-index.xml
-@@ -0,0 +1,321 @@
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-+
-diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/lint-cache/lintVitalAnalyzeRelease/private-apis-18-7541949.bin b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/lint-cache/lintVitalAnalyzeRelease/private-apis-18-7541949.bin
-new file mode 100644
-index 0000000..0d21700
-Binary files /dev/null and b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/lint-cache/lintVitalAnalyzeRelease/private-apis-18-7541949.bin differ
-diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/lint-cache/lintVitalAnalyzeRelease/sdk_index/snapshot.gz b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/lint-cache/lintVitalAnalyzeRelease/sdk_index/snapshot.gz
-new file mode 100644
-index 0000000..9bfdca2
-Binary files /dev/null and b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/lint-cache/lintVitalAnalyzeRelease/sdk_index/snapshot.gz differ
diff --git a/node_modules/rn-barcode-zxing-scan/android/build/intermediates/lint_model/release/generateReleaseLintModel/module.xml b/node_modules/rn-barcode-zxing-scan/android/build/intermediates/lint_model/release/generateReleaseLintModel/module.xml
new file mode 100644
index 0000000..385fb17
@@ -54655,7 +52981,7 @@ new file mode 100644
index 0000000..b33df0f
Binary files /dev/null and b/node_modules/rn-barcode-zxing-scan/android/build/tmp/compileReleaseJavaWithJavac/previous-compilation-data.bin differ
diff --git a/node_modules/rn-barcode-zxing-scan/android/src/main/java/com/reactlibrary/BarcodeZxingScanModule.java b/node_modules/rn-barcode-zxing-scan/android/src/main/java/com/reactlibrary/BarcodeZxingScanModule.java
-index fe94d5b..23dcf0d 100644
+index fe94d5b..23f1314 100644
--- a/node_modules/rn-barcode-zxing-scan/android/src/main/java/com/reactlibrary/BarcodeZxingScanModule.java
+++ b/node_modules/rn-barcode-zxing-scan/android/src/main/java/com/reactlibrary/BarcodeZxingScanModule.java
@@ -4,11 +4,14 @@ import android.app.Activity;
@@ -54673,13 +52999,14 @@ index fe94d5b..23dcf0d 100644
import com.google.zxing.integration.android.IntentIntegrator;
import com.google.zxing.integration.android.IntentResult;
-@@ -16,11 +19,17 @@ public class BarcodeZxingScanModule extends ReactContextBaseJavaModule implement
+@@ -16,11 +19,18 @@ public class BarcodeZxingScanModule extends ReactContextBaseJavaModule implement
private final ReactApplicationContext reactContext;
private Callback mCallback;
+ private static final Object callbackLock = new Object();
+ private static String lastProcessedText = null;
+ private static ReactApplicationContext staticReactContext = null;
++ private static String customStatusMessage = null;
public BarcodeZxingScanModule(ReactApplicationContext reactContext) {
super(reactContext);
@@ -54691,7 +53018,20 @@ index fe94d5b..23dcf0d 100644
}
@Override
-@@ -54,6 +63,81 @@ public class BarcodeZxingScanModule extends ReactContextBaseJavaModule implement
+@@ -47,13 +57,106 @@ public class BarcodeZxingScanModule extends ReactContextBaseJavaModule implement
+ integrator.setOrientationLocked(true);
+ integrator.setBeepEnabled(false); // Disable beep sound
+ integrator.setCaptureActivity(ContinuousCaptureActivity.class);
+- integrator.initiateScan();
++ // Pass custom status message via Intent
++ Intent intent = integrator.createScanIntent();
++ if (customStatusMessage != null && !customStatusMessage.isEmpty()) {
++ intent.putExtra("CUSTOM_STATUS_MESSAGE", customStatusMessage);
++ }
++ currentActivity.startActivityForResult(intent, IntentIntegrator.REQUEST_CODE);
+ reactContext.addActivityEventListener(this); // Ensure we handle the result
+ } else {
+ callback.invoke("Error: Activity is null");
}
}
@@ -54713,6 +53053,10 @@ index fe94d5b..23dcf0d 100644
+ // Add extra to indicate continuous mode
+ Intent intent = integrator.createScanIntent();
+ intent.putExtra("CONTINUOUS_SCAN", true);
++ // Pass custom status message via Intent if set
++ if (customStatusMessage != null && !customStatusMessage.isEmpty()) {
++ intent.putExtra("CUSTOM_STATUS_MESSAGE", customStatusMessage);
++ }
+ // Use the REQUEST_CODE constant from IntentIntegrator
+ currentActivity.startActivityForResult(intent, IntentIntegrator.REQUEST_CODE);
+ // Invoke callback once to acknowledge scanner is starting
@@ -54769,15 +53113,24 @@ index fe94d5b..23dcf0d 100644
+ // Update the progress text overlay in the continuous scan activity
+ ContinuousCaptureActivity.updateProgressText(text);
+ }
++
++ @ReactMethod
++ public void setStatusMessage(String message) {
++ // Set custom status message for the scanner
++ // This will be used when opening the scanner activity
++ synchronized (callbackLock) {
++ customStatusMessage = message;
++ }
++ }
+
@Override
public void onActivityResult(Activity activity, int requestCode, int resultCode, Intent data) {
if (mCallback != null) {
diff --git a/node_modules/rn-barcode-zxing-scan/android/src/main/java/com/reactlibrary/ContinuousCaptureActivity.java b/node_modules/rn-barcode-zxing-scan/android/src/main/java/com/reactlibrary/ContinuousCaptureActivity.java
-index 8898813..82d7b72 100644
+index 8898813..166a9c8 100644
--- a/node_modules/rn-barcode-zxing-scan/android/src/main/java/com/reactlibrary/ContinuousCaptureActivity.java
+++ b/node_modules/rn-barcode-zxing-scan/android/src/main/java/com/reactlibrary/ContinuousCaptureActivity.java
-@@ -1,12 +1,288 @@
+@@ -1,12 +1,387 @@
package com.reactlibrary;
+import android.content.Intent;
@@ -54815,11 +53168,16 @@ index 8898813..82d7b72 100644
+ private boolean continuousScanningSetup = false;
+ private CaptureManager captureManager;
+ private TextView progressTextView;
++ private String customStatusMessage = null;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ Intent intent = getIntent();
+ isContinuousMode = intent != null && intent.getBooleanExtra(CONTINUOUS_SCAN_EXTRA, false);
++ // Read custom status message from Intent
++ if (intent != null && intent.hasExtra("CUSTOM_STATUS_MESSAGE")) {
++ customStatusMessage = intent.getStringExtra("CUSTOM_STATUS_MESSAGE");
++ }
+
+ if (isContinuousMode) {
+ // Store static reference to this activity instance
@@ -54867,6 +53225,11 @@ index 8898813..82d7b72 100644
+
+ super.onResume();
+
++ // Set custom status message if provided (for both single and continuous mode)
++ if (customStatusMessage != null && !customStatusMessage.isEmpty()) {
++ setCustomStatusMessage(customStatusMessage);
++ }
++
+ if (isContinuousMode && !continuousScanningSetup && barcodeView != null) {
+ // Set up continuous scanning after parent onResume completes
+ try {
@@ -55036,7 +53399,7 @@ index 8898813..82d7b72 100644
+ e.printStackTrace();
+ }
+ }
-
++
+ @Override
+ protected void onStop() {
+ // Clear static reference when activity stops (before destroy)
@@ -55050,7 +53413,7 @@ index 8898813..82d7b72 100644
+ }
+ super.onStop();
+ }
-
++
+ // Helper method to find DecoratedBarcodeView in view hierarchy
+ private DecoratedBarcodeView findBarcodeView(View view) {
+ if (view instanceof DecoratedBarcodeView) {
@@ -55066,5 +53429,94 @@ index 8898813..82d7b72 100644
+ }
+ }
+ return null;
++ }
++
++ /**
++ * Set custom status message on the ViewfinderView
++ * Uses reflection to access the status TextView in the ZXing library
++ */
++ private void setCustomStatusMessage(String message) {
++ try {
++ // Find the DecoratedBarcodeView
++ View rootView = findViewById(android.R.id.content);
++ if (rootView == null) {
++ return;
++ }
++
++ DecoratedBarcodeView barcodeView = findBarcodeView(rootView);
++ if (barcodeView == null) {
++ return;
++ }
++
++ // Use reflection to access the ViewfinderView and its status TextView
++ // The ViewfinderView is typically a child of DecoratedBarcodeView
++ ViewGroup barcodeViewGroup = (ViewGroup) barcodeView;
++ for (int i = 0; i < barcodeViewGroup.getChildCount(); i++) {
++ View child = barcodeViewGroup.getChildAt(i);
++ // Look for ViewfinderView (com.journeyapps.barcodescanner.ViewfinderView)
++ if (child.getClass().getName().contains("ViewfinderView")) {
++ // Try to find TextView inside ViewfinderView
++ if (child instanceof ViewGroup) {
++ ViewGroup viewfinderGroup = (ViewGroup) child;
++ for (int j = 0; j < viewfinderGroup.getChildCount(); j++) {
++ View viewfinderChild = viewfinderGroup.getChildAt(j);
++ if (viewfinderChild instanceof TextView) {
++ TextView statusTextView = (TextView) viewfinderChild;
++ // Check if this is the status text view (usually has specific ID or is the only TextView)
++ // Set the custom message
++ statusTextView.setText(message);
++ return;
++ }
++ }
++ }
++ // Alternative: Try to use reflection to access statusView field
++ try {
++ Field statusViewField = child.getClass().getDeclaredField("statusView");
++ statusViewField.setAccessible(true);
++ TextView statusView = (TextView) statusViewField.get(child);
++ if (statusView != null) {
++ statusView.setText(message);
++ return;
++ }
++ } catch (NoSuchFieldException e) {
++ // Field doesn't exist, try other approaches
++ }
++ }
++ }
+
++ // Fallback: Try to find any TextView that might be the status view
++ // by checking if it contains the default status message
++ TextView statusTextView = findStatusTextView(rootView);
++ if (statusTextView != null) {
++ statusTextView.setText(message);
++ }
++ } catch (Exception e) {
++ // If we can't set the custom message, that's okay - scanner will still work
++ e.printStackTrace();
++ }
++ }
+
++ /**
++ * Helper method to find the status TextView in the view hierarchy
++ */
++ private TextView findStatusTextView(View view) {
++ if (view instanceof TextView) {
++ TextView textView = (TextView) view;
++ String text = textView.getText().toString();
++ // Check if this TextView contains the default status message
++ if (text.contains("barcode") || text.contains("viewfinder") || text.contains("scan")) {
++ return textView;
++ }
++ }
++ if (view instanceof ViewGroup) {
++ ViewGroup group = (ViewGroup) view;
++ for (int i = 0; i < group.getChildCount(); i++) {
++ TextView found = findStatusTextView(group.getChildAt(i));
++ if (found != null) {
++ return found;
++ }
++ }
++ }
++ return null;
+ }
}
diff --git a/screens/MobileNostrPairing.foss.tsx b/screens/MobileNostrPairing.foss.tsx
deleted file mode 100644
index d9e4bf7..0000000
--- a/screens/MobileNostrPairing.foss.tsx
+++ /dev/null
@@ -1,6032 +0,0 @@
-/* eslint-disable react-native/no-inline-styles */
-import React, {useState, useEffect, useRef} from 'react';
-import {
- View,
- Text,
- StyleSheet,
- Alert,
- Image,
- TouchableOpacity,
- Modal,
- TextInput,
- ScrollView,
- Platform,
- KeyboardAvoidingView,
- NativeEventEmitter,
- EmitterSubscription,
- Animated,
- Keyboard,
-} from 'react-native';
-import Share from 'react-native-share';
-import {NativeModules} from 'react-native';
-import DeviceInfo from 'react-native-device-info';
-import EncryptedStorage from 'react-native-encrypted-storage';
-import QRCode from 'react-native-qrcode-svg';
-import Clipboard from '@react-native-clipboard/clipboard';
-import BarcodeZxingScan from 'rn-barcode-zxing-scan';
-import * as Progress from 'react-native-progress';
-import {CommonActions, RouteProp, useRoute} from '@react-navigation/native';
-import {SafeAreaView} from 'react-native-safe-area-context';
-import Big from 'big.js';
-import {dbg, HapticFeedback, getNostrRelays, getKeyshareLabel, hexToString, getDerivePathForNetwork} from '../utils';
-import {useTheme} from '../theme';
-import {useUser} from '../context/UserContext';
-import LocalCache from '../services/LocalCache';
-import {WalletService} from '../services/WalletService';
-import RNFS from 'react-native-fs';
-
-const {BBMTLibNativeModule} = NativeModules;
-
-type RouteParams = {
- mode?: string; // 'duo' | 'trio' | 'send_btc' | 'sign_psbt'
- addressType?: string;
- toAddress?: string;
- satoshiAmount?: string;
- fiatAmount?: string;
- satoshiFees?: string;
- fiatFees?: string;
- selectedCurrency?: string;
- spendingHash?: string;
- psbtBase64?: string; // For PSBT signing mode
- derivePath?: string; // BIP32 derivation path for PSBT
-};
-
-const MobileNostrPairing = ({navigation}: any) => {
- const route = useRoute>();
- const isSendBitcoin = route.params?.mode === 'send_btc';
- const isSignPSBT = route.params?.mode === 'sign_psbt';
- const setupMode = route.params?.mode;
- const addressType = route.params?.addressType;
- // In send mode, determine isTrio from keyshare (3 devices = trio, 2 devices = duo)
- // In keygen mode, use setupMode
- const [isTrio, setIsTrio] = useState(setupMode === 'trio');
- const {theme} = useTheme();
- const {activeAddress} = useUser();
- const ppmFile = `${RNFS.DocumentDirectoryPath}/ppm.json`;
-
- // Nostr Identity
- const [localNsec, setLocalNsec] = useState('');
- const [localNpub, setLocalNpub] = useState('');
- const [deviceName, setDeviceName] = useState('');
-
- // Relays - Load from cache or use defaults
- const [relaysInput, setRelaysInput] = useState('');
- const [relays, setRelays] = useState([]);
-
- // Partial nonce (random UUID/number generated on each device)
- const [partialNonce, setPartialNonce] = useState('');
-
- // Peer Connections (for duo: 1 peer, for trio: 2 peers)
- const [peerConnectionDetails1, setPeerConnectionDetails1] =
- useState('');
- const [peerNpub1, setPeerNpub1] = useState('');
- const [peerDeviceName1, setPeerDeviceName1] = useState('');
- const [peerNonce1, setPeerNonce1] = useState('');
- const [peerConnectionDetails2, setPeerConnectionDetails2] =
- useState('');
- const [peerNpub2, setPeerNpub2] = useState('');
- const [peerDeviceName2, setPeerDeviceName2] = useState('');
- const [peerNonce2, setPeerNonce2] = useState('');
- const [peerInputError1, setPeerInputError1] = useState('');
- const [peerInputError2, setPeerInputError2] = useState('');
- const [peerInputValidating1, setPeerInputValidating1] =
- useState(false);
- const [peerInputValidating2, setPeerInputValidating2] =
- useState(false);
-
- // Session (generated deterministically)
- const [sessionID, setSessionID] = useState('');
- const [sessionKey, setSessionKey] = useState('');
- const [chaincode, setChaincode] = useState('');
-
- // Progress
- const [progress, setProgress] = useState(0);
- const [status, setStatus] = useState('');
- const [isPairing, setIsPairing] = useState(false);
- const [isKeygenReady, setIsKeygenReady] = useState(false); // Manual toggle for "other devices ready"
- const [isKeysignReady, setIsKeysignReady] = useState(false); // Manual toggle for PSBT/send signing readiness
- const [canStartKeygen, setCanStartKeygen] = useState(false); // Auto-calculated: all conditions met
- const [mpcDone, setMpcDone] = useState(false);
- const [psbtDetails, setPsbtDetails] = useState<{
- inputs: Array<{txid: string; vout: number; amount: number}>;
- outputs: Array<{address: string; amount: number}>;
- fee: number;
- totalInput: number;
- totalOutput: number;
- derivePath?: string;
- derivePaths?: string[];
- } | null>(null);
- const [isPreParamsReady, setIsPreParamsReady] = useState(false);
- const [isPreparing, setIsPreparing] = useState(false);
- const [isPrepared, setIsPrepared] = useState(false);
- const [prepCounter, setPrepCounter] = useState(0);
- const progressAnimation = useRef(new Animated.Value(0)).current;
- const progressAnimationLoop = useRef(
- null,
- );
-
- // Backup state
- const [isBackupModalVisible, setIsBackupModalVisible] = useState(false);
- const [password, setPassword] = useState('');
- const [confirmPassword, setConfirmPassword] = useState('');
- const [passwordVisible, setPasswordVisible] = useState(false);
- const [confirmPasswordVisible, setConfirmPasswordVisible] = useState(false);
- const [passwordStrength, setPasswordStrength] = useState(0);
- const [passwordErrors, setPasswordErrors] = useState([]);
- const [backupChecks, setBackupChecks] = useState({
- deviceOne: false,
- deviceTwo: false,
- deviceThree: false,
- });
-
- // Keyshare mapping (based on sorted npubs)
- const [keyshareMapping, setKeyshareMapping] = useState<{
- keyshare1?: {npub: string; deviceName: string; isLocal: boolean};
- keyshare2?: {npub: string; deviceName: string; isLocal: boolean};
- keyshare3?: {npub: string; deviceName: string; isLocal: boolean};
- }>({});
-
- // Send mode: device selection (for trio mode)
- const [selectedPeerNpub, setSelectedPeerNpub] = useState('');
- const [sendModeDevices, setSendModeDevices] = useState<
- Array<{
- keyshareLabel: string;
- npub: string;
- isLocal: boolean;
- }>
- >([]);
-
- // QR Scanner / QR Share
- // FOSS version: Only use ref, no state needed since we don't use iOS modal
- const scanningForPeerRef = useRef<1 | 2>(1);
- const [isQRModalVisible, setIsQRModalVisible] = useState(false);
- const [showRelayConfig, setShowRelayConfig] = useState(false);
- const [showHelpModal, setShowHelpModal] = useState(false);
- const connectionQrRef = useRef(null);
-
- // Connection details for sharing (hex encoded)
- const connectionDetails = React.useMemo(() => {
- if (!localNpub || !deviceName || !partialNonce) {
- return '';
- }
- const plaintext = `${localNpub}:${deviceName}:${partialNonce}`;
- // Convert to hex encoding
- let hex = '';
- for (let i = 0; i < plaintext.length; i++) {
- const charCode = plaintext.charCodeAt(i);
- hex += charCode.toString(16).padStart(2, '0');
- }
- return hex;
- }, [localNpub, deviceName, partialNonce]);
-
- // Load default relays on mount (from cache if available, otherwise fetch dynamically)
- useEffect(() => {
- const loadRelays = async () => {
- try {
- // Use getNostrRelays which handles cache and fetching
- const fetchedRelays = await getNostrRelays(false);
- const relaysCSV = fetchedRelays.join(',');
- // Convert CSV to newline-separated for multiline display
- const relaysForDisplay = relaysCSV.split(',').join('\n');
- setRelaysInput(relaysForDisplay);
- setRelays(fetchedRelays);
- } catch (error) {
- dbg('Error loading relays:', error);
- // Fallback to defaults on error
- const defaults = [
- 'wss://bbw-nostr.xyz',
- 'wss://nostr.hifish.org',
- 'wss://nostr.xxi.quest',
- ];
- const defaultsCSV = defaults.join(',');
- const defaultsForDisplay = defaultsCSV.split(',').join('\n');
- setRelaysInput(defaultsForDisplay);
- setRelays(defaults);
- }
- };
- loadRelays();
- }, []);
-
- // Update relays when input changes (support both comma and newline separation)
- useEffect(() => {
- const parsed = relaysInput
- .split(/[,\n]/)
- .map(r => r.trim())
- .filter(Boolean);
- setRelays(parsed);
- }, [relaysInput]);
-
- // Initialize device name and generate keypair on mount (only for keygen mode)
- useEffect(() => {
- const initialize = async () => {
- try {
- const name = await DeviceInfo.getDeviceName();
- setDeviceName(name);
- // Generate random partial nonce (UUID or random number)
- // Using a combination of timestamp and random for uniqueness
- const randomNonce = await BBMTLibNativeModule.sha256(
- `${Date.now()}-${Math.random()}`,
- );
- setPartialNonce(randomNonce);
- dbg('Generated partialNonce:', randomNonce);
- // Only generate new keypair if not in send/sign mode (send/sign mode loads from keyshare)
- if (!isSendBitcoin && !isSignPSBT) {
- await generateLocalKeypair();
- }
- } catch (error) {
- dbg('Error initializing:', error);
- Alert.alert('Error', 'Failed to initialize device');
- }
- };
- initialize();
- }, [isSendBitcoin, isSignPSBT]);
-
- // Generate session params when peer connections are ready
- useEffect(() => {
- if (localNpub && deviceName && partialNonce) {
- if (isSendBitcoin || isSignPSBT) {
- // For send BTC / sign PSBT, we need balance - will be generated when starting
- return;
- }
- // For keygen, generate when we have peer(s) with nonces
- if (isTrio) {
- if (
- peerNpub1 &&
- peerDeviceName1 &&
- peerNonce1 &&
- peerNpub2 &&
- peerDeviceName2 &&
- peerNonce2
- ) {
- generateKeygenSessionParams();
- }
- } else {
- if (peerNpub1 && peerDeviceName1 && peerNonce1) {
- generateKeygenSessionParams();
- }
- }
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [
- localNpub,
- deviceName,
- partialNonce,
- peerNpub1,
- peerDeviceName1,
- peerNonce1,
- peerNpub2,
- peerDeviceName2,
- peerNonce2,
- isTrio,
- isSendBitcoin,
- ]);
-
- // Check if all conditions are met to enable start keygen button
- // (requires preparams ready AND manual confirmation that other devices are ready)
- useEffect(() => {
- const ready =
- localNpub &&
- deviceName &&
- relays.length > 0 &&
- sessionID &&
- sessionKey &&
- chaincode &&
- isPreParamsReady &&
- isKeygenReady && // Manual confirmation that other devices are ready
- ((isTrio &&
- peerNpub1 &&
- peerDeviceName1 &&
- peerNpub2 &&
- peerDeviceName2) ||
- (!isTrio && peerNpub1 && peerDeviceName1));
- setCanStartKeygen(!!ready);
- }, [
- localNpub,
- deviceName,
- relays,
- sessionID,
- sessionKey,
- chaincode,
- isPreParamsReady,
- isKeygenReady, // Include manual toggle
- peerNpub1,
- peerDeviceName1,
- peerNpub2,
- peerDeviceName2,
- isTrio,
- ]);
-
- // Toggle function for manual "other devices ready" confirmation
- const toggleKeygenReady = () => {
- setIsKeygenReady(!isKeygenReady);
- };
-
- const toggleKeysignReady = () => {
- HapticFeedback.medium();
- setIsKeysignReady(!isKeysignReady);
- };
-
- // Parse PSBT details when PSBT is available
- useEffect(() => {
- const parsePSBT = async () => {
- if (isSignPSBT && route.params?.psbtBase64) {
- try {
- dbg('Parsing PSBT details for summary...');
- const detailsJson = await BBMTLibNativeModule.parsePSBTDetails(
- route.params.psbtBase64,
- );
-
- if (detailsJson.startsWith('error') || detailsJson.includes('failed')) {
- dbg('Failed to parse PSBT details:', detailsJson);
- setPsbtDetails(null);
- return;
- }
-
- const details = JSON.parse(detailsJson);
- setPsbtDetails({
- inputs: details.inputs || [],
- outputs: details.outputs || [],
- fee: details.fee || 0,
- totalInput: details.totalInput || 0,
- totalOutput: details.totalOutput || 0,
- derivePath: details.derivePath,
- derivePaths: details.derivePaths || [],
- });
- dbg('PSBT details parsed:', {
- inputs: details.inputs?.length || 0,
- outputs: details.outputs?.length || 0,
- fee: details.fee,
- });
- } catch (error) {
- dbg('Error parsing PSBT details:', error);
- setPsbtDetails(null);
- }
- } else {
- setPsbtDetails(null);
- }
- };
-
- parsePSBT();
- }, [isSignPSBT, route.params?.psbtBase64]);
-
- // Listen to native module events for progress tracking
- useEffect(() => {
- const eventEmitter = new NativeEventEmitter(BBMTLibNativeModule);
- const keygenSteps = isTrio ? 25 : 18;
- const keysignSteps = 36;
- let utxoRange = 0;
- let utxoIndex = 0;
- let utxoCount = 0;
- const processHook = (message: string) => {
- try {
- const msg = JSON.parse(message);
- if (msg.type === 'keygen') {
- if (msg.done) {
- dbg('progress - keygen done');
- setProgress(100);
- setMpcDone(true);
- // Don't navigate away, let the backup UI handle it
- } else {
- dbg(
- 'progress - keygen: ',
- Math.round((100 * msg.step) / keygenSteps),
- 'step',
- msg.step,
- 'time',
- new Date(msg.time),
- );
- setProgress(Math.round((100 * msg.step) / keygenSteps));
- dbg('keygen_hook_info:', msg.info);
- const statusDot =
- msg.step % 3 === 0 ? '.' : msg.step % 3 === 1 ? '..' : '...';
- setStatus('Processing cryptographic operations' + statusDot);
- }
- } else if (msg.type === 'btc_send') {
- if (msg.done) {
- setProgress(100);
- }
- if (msg.utxo_total > 0) {
- utxoCount = msg.utxo_total;
- utxoIndex = msg.utxo_current;
- utxoRange = 100 / utxoCount;
- dbg('progress send_btc', {
- utxoCount,
- utxoIndex,
- utxoRange,
- });
- }
- dbg('btc_send_hook_info:', msg.info);
- const statusDot =
- msg.step % 3 === 0 ? '.' : msg.step % 3 === 1 ? '..' : '...';
- setStatus('Processing cryptographic operations' + statusDot);
- } else if (msg.type === 'keysign') {
- const prgUTXO = (utxoIndex - 1) * utxoRange;
- const progressValue =
- utxoCount > 0
- ? Math.round(prgUTXO + (utxoRange * msg.step) / keysignSteps)
- : Math.round((100 * msg.step) / keysignSteps);
- dbg(
- 'progress - keysign: ',
- progressValue,
- 'prgUTXO',
- prgUTXO,
- 'step',
- msg.step,
- 'range',
- utxoRange,
- 'time',
- new Date(msg.time),
- );
- setProgress(progressValue);
- dbg('keysign_hook_info:', msg.info);
- const statusDot =
- msg.step % 3 === 0 ? '.' : msg.step % 3 === 1 ? '..' : '...';
- setStatus('Processing cryptographic operations' + statusDot);
- if (msg.done) {
- setProgress(100);
- setMpcDone(true);
- }
- }
- } catch {
- // If parsing fails, it might be a log message, just log it
- dbg('TSS log:', message);
- }
- };
-
- const subscription: EmitterSubscription = eventEmitter.addListener(
- Platform.OS === 'android' ? 'BBMT_DROID' : 'BBMT_APPLE',
- (event: any) => {
- if (event.tag === 'TssHook') {
- processHook(event.message);
- } else if (event.tag === 'GoLog') {
- dbg('TSS:', event.message);
- }
- },
- );
-
- return () => {
- subscription.remove();
- };
- }, [isTrio]);
-
- // Load keyshare and derive device info in send/sign mode
- useEffect(() => {
- if (!isSendBitcoin && !isSignPSBT) return;
-
- const loadKeyshareData = async () => {
- try {
- const keyshareJSON = await EncryptedStorage.getItem('keyshare');
- if (!keyshareJSON) {
- dbg('No keyshare found in send/sign mode');
- setSendModeDevices([]);
- return;
- }
-
- const keyshare = JSON.parse(keyshareJSON);
- if (!keyshare.keygen_committee_keys || !keyshare.local_party_key) {
- dbg('Keyshare missing required fields');
- setSendModeDevices([]);
- return;
- }
-
- // Determine if trio mode based on number of devices in keyshare
- const numDevices = keyshare.keygen_committee_keys?.length || 0;
- const isTrioMode = numDevices === 3;
- setIsTrio(isTrioMode);
- dbg(
- 'Send mode - detected',
- isTrioMode ? 'trio' : 'duo',
- 'mode from keyshare (',
- numDevices,
- 'devices)',
- );
-
- // Get local npub from keyshare
- const localNpubFromKeyshare = keyshare.nostr_npub || '';
-
- // Get local nsec from keyshare
- // The nsec might be stored as hex-encoded bytes OR already in bech32 format
- const nsecFromKeyshare = keyshare.nsec || '';
-
- if (nsecFromKeyshare) {
- // Check if it's already in bech32 format
- if (nsecFromKeyshare.startsWith('nsec1')) {
- // Already in correct format
- setLocalNsec(nsecFromKeyshare);
- dbg(
- 'Nsec from keyshare (already bech32):',
- nsecFromKeyshare.substring(0, 20) + '...',
- );
- } else {
- // Try to decode from hex
- try {
- const decodedNsec = hexToString(nsecFromKeyshare);
- dbg(
- 'Decoded nsec from hex:',
- decodedNsec.substring(0, 20) + '...',
- );
-
- // Verify it's a valid nsec format
- if (decodedNsec.startsWith('nsec1')) {
- setLocalNsec(decodedNsec);
- dbg('Nsec set successfully');
- } else {
- dbg(
- 'Warning: Decoded nsec does not start with nsec1:',
- decodedNsec.substring(0, 50),
- );
- }
- } catch (error) {
- dbg('Error decoding nsec from hex:', error);
- }
- }
- } else {
- dbg('Warning: No nsec found in keyshare');
- }
-
- // Set local npub if available
- if (localNpubFromKeyshare) {
- setLocalNpub(localNpubFromKeyshare);
- }
-
- // Sort keygen_committee_keys to match the order used for keyshare labels
- const sortedKeys = [...keyshare.keygen_committee_keys].sort();
-
- // Build device list IMMEDIATELY with available data
- const devices: Array<{
- keyshareLabel: string;
- npub: string;
- isLocal: boolean;
- }> = [];
-
- for (let i = 0; i < sortedKeys.length; i++) {
- const hexKey = sortedKeys[i];
- const isLocal = hexKey === keyshare.local_party_key;
- const keyshareLabel = `KeyShare${i + 1}`;
-
- let npub = '';
- if (isLocal) {
- // Use local npub if available, otherwise use shortened hex
- npub =
- localNpubFromKeyshare ||
- hexKey.substring(0, 12) +
- '...' +
- hexKey.substring(hexKey.length - 8);
- } else {
- // For other devices, use shortened hex as placeholder, will update async
- npub =
- hexKey.substring(0, 12) +
- '...' +
- hexKey.substring(hexKey.length - 8);
- }
-
- devices.push({
- keyshareLabel,
- npub,
- isLocal,
- });
- }
-
- // Set devices immediately so UI can render
- setSendModeDevices(devices);
- dbg('Send mode devices loaded (initial):', devices);
-
- // Now update npubs for other devices asynchronously
- const updatedDevices = [...devices];
- for (let i = 0; i < sortedKeys.length; i++) {
- const hexKey = sortedKeys[i];
- const isLocal = hexKey === keyshare.local_party_key;
-
- if (!isLocal) {
- try {
- // Validate hex key format before calling hexToNpub
- const hexPattern = /^[0-9a-fA-F]+$/;
- if (!hexPattern.test(hexKey)) {
- dbg(
- 'Invalid hex key format, skipping npub conversion:',
- hexKey.substring(0, 20) + '...',
- );
- continue;
- }
-
- const result = await BBMTLibNativeModule.hexToNpub(hexKey);
- if (
- result &&
- typeof result === 'string' &&
- result.startsWith('npub1')
- ) {
- const oldNpub = updatedDevices[i].npub;
- updatedDevices[i].npub = result;
- dbg(
- 'Updated npub for device:',
- result.substring(0, 20) + '...',
- );
- // Update state with new npub
- setSendModeDevices([...updatedDevices]);
-
- // If this device was selected (by placeholder), update selectedPeerNpub to full npub
- // Use a callback to access current selectedPeerNpub state
- setSelectedPeerNpub(current => {
- if (
- current === oldNpub ||
- (oldNpub && result.startsWith(oldNpub.substring(0, 20)))
- ) {
- dbg(
- 'Updated selectedPeerNpub to full npub:',
- result.substring(0, 20) + '...',
- );
- return result;
- }
- return current;
- });
- }
- } catch (error) {
- dbg('Error converting hex to npub:', error);
- // Keep the shortened hex as fallback
- }
- }
- }
- } catch (error: any) {
- dbg('Error loading keyshare data:', error);
- setSendModeDevices([]);
- }
- };
-
- loadKeyshareData();
- }, [isSendBitcoin, isSignPSBT]);
-
- // Auto-select peer in duo mode, or first peer in trio mode (deterministic)
- // Only auto-selects if no selection exists - never overrides user's manual selection
- useEffect(() => {
- if ((isSendBitcoin || isSignPSBT) && sendModeDevices.length > 0) {
- const otherDevices = sendModeDevices.filter(d => !d.isLocal);
-
- // Only auto-select if no peer is currently selected
- if (!selectedPeerNpub) {
- if (isTrio && otherDevices.length >= 2) {
- // In trio mode, deterministically select the first peer (sorted by npub)
- // This ensures both devices select the same peer by default
- // User can still manually change the selection
- const sortedOtherDevices = [...otherDevices].sort((a, b) => {
- // Sort by npub (handle both full and shortened npubs)
- const npubA = a.npub || '';
- const npubB = b.npub || '';
- return npubA.localeCompare(npubB);
- });
- const firstPeer = sortedOtherDevices[0];
- if (firstPeer && firstPeer.npub) {
- setSelectedPeerNpub(firstPeer.npub);
- dbg(
- 'Auto-selected first peer in trio mode (deterministic, user can change):',
- firstPeer.npub.substring(0, 20) + '...',
- );
- }
- } else if (!isTrio && otherDevices.length >= 1) {
- // In duo mode, auto-select the only other device
- const otherDevice = otherDevices[0];
- if (otherDevice && otherDevice.npub) {
- setSelectedPeerNpub(otherDevice.npub);
- dbg(
- 'Auto-selected peer in duo mode:',
- otherDevice.npub.substring(0, 20) + '...',
- );
- }
- }
- }
- }
- }, [isSendBitcoin, isSignPSBT, isTrio, sendModeDevices, selectedPeerNpub]);
-
- const generateLocalKeypair = async () => {
- try {
- const keypairJSON = await BBMTLibNativeModule.nostrKeypair();
- const keypair = JSON.parse(keypairJSON);
- setLocalNsec(keypair.nsec);
- setLocalNpub(keypair.npub);
- dbg(
- 'Generated Nostr keypair - nsec:',
- keypair.nsec?.substring(0, 10) + '...',
- );
- dbg('Generated Nostr keypair - npub:', keypair.npub);
- dbg(
- 'Generated Nostr keypair - npub format check:',
- keypair.npub?.startsWith('npub1')
- ? 'bech32 (correct)'
- : 'NOT bech32 (incorrect - should start with npub1)',
- );
- } catch (error: any) {
- dbg('Error generating keypair:', error);
- Alert.alert('Error', 'Failed to generate Nostr keypair');
- }
- };
-
- // Helper function to shorten npub for display
- const shortenNpub = (
- npub: string,
- startLen: number = 8,
- endLen: number = 4,
- ): string => {
- if (!npub || npub.length <= startLen + endLen) {
- return npub;
- }
- return `${npub.substring(0, startLen)}...${npub.substring(
- npub.length - endLen,
- )}`;
- };
-
- // Helper function to format connection details for display
- const formatConnectionDisplay = (
- npub: string,
- deviceNameValue: string,
- ): string => {
- if (!npub || !deviceNameValue) {
- return '';
- }
- return `${deviceNameValue}@${shortenNpub(npub)}`;
- };
-
- const parseConnectionDetails = async (
- input: string,
- ): Promise<{
- npub: string;
- deviceName: string;
- partialNonce: string;
- } | null> => {
- const trimmed = input.trim();
- dbg('parseConnectionDetails: input =', trimmed.substring(0, 50) + '...');
-
- if (!trimmed) {
- dbg('parseConnectionDetails: empty input');
- return null;
- }
-
- // Try to decode as hex first
- let decoded = '';
- try {
- // Check if it looks like hex (even length, only hex chars)
- const hexPattern = /^[0-9a-fA-F]+$/;
- if (hexPattern.test(trimmed) && trimmed.length % 2 === 0) {
- // Decode hex to string
- for (let i = 0; i < trimmed.length; i += 2) {
- const hexByte = trimmed.substr(i, 2);
- const charCode = parseInt(hexByte, 16);
- decoded += String.fromCharCode(charCode);
- }
- dbg(
- 'parseConnectionDetails: decoded hex to:',
- decoded.substring(0, 50) + '...',
- );
- } else {
- // Not hex, try as plaintext (backward compatibility)
- decoded = trimmed;
- dbg('parseConnectionDetails: treating as plaintext');
- }
- } catch (error) {
- dbg('parseConnectionDetails: error decoding hex:', error);
- return null;
- }
-
- const parts = decoded.split(':');
- dbg('parseConnectionDetails: split parts count =', parts.length);
-
- if (parts.length !== 3) {
- dbg(
- 'parseConnectionDetails: invalid format - expected 3 parts (npub:deviceName:partialNonce), got',
- parts.length,
- );
- return null;
- }
-
- let [npub, peerDeviceName, peerPartialNonce] = parts;
- let trimmedNpub = npub.trim();
- const trimmedDeviceName = peerDeviceName.trim();
- const trimmedNonce = peerPartialNonce.trim();
-
- dbg('parseConnectionDetails: npub =', trimmedNpub.substring(0, 20) + '...');
- dbg('parseConnectionDetails: deviceName =', trimmedDeviceName);
- dbg('parseConnectionDetails: partialNonce =', trimmedNonce);
-
- // Check if it's a hex string (64 hex characters) and try to convert to npub
- if (!trimmedNpub.startsWith('npub1')) {
- // Check if it's a hex string
- const hexPattern = /^[0-9a-fA-F]{64}$/;
- if (hexPattern.test(trimmedNpub)) {
- dbg(
- 'parseConnectionDetails: detected hex string, attempting to convert to npub',
- );
- try {
- // Try to convert hex to npub using native module
- // First, we need to check if there's a conversion function
- // For now, we'll show a helpful error message
- dbg(
- 'parseConnectionDetails: hex string detected but conversion not available',
- );
- return null; // Will show error message below
- } catch (error) {
- dbg('parseConnectionDetails: error converting hex to npub:', error);
- return null;
- }
- } else {
- dbg(
- 'parseConnectionDetails: invalid npub - does not start with npub1 and is not valid hex',
- );
- return null;
- }
- }
-
- if (trimmedNpub.length < 10) {
- dbg('parseConnectionDetails: invalid npub - too short');
- return null;
- }
-
- if (trimmedDeviceName.length === 0) {
- dbg('parseConnectionDetails: invalid device name - empty');
- return null;
- }
-
- if (trimmedNonce.length === 0) {
- dbg('parseConnectionDetails: invalid partialNonce - empty');
- return null;
- }
-
- dbg(
- 'parseConnectionDetails: valid! npub =',
- trimmedNpub.substring(0, 20) + '...',
- 'deviceName =',
- trimmedDeviceName,
- 'partialNonce =',
- trimmedNonce,
- );
- return {
- npub: trimmedNpub,
- deviceName: trimmedDeviceName,
- partialNonce: trimmedNonce,
- };
- };
-
- const handlePeerConnectionInput = async (input: string, peerNum: 1 | 2) => {
- dbg(`handlePeerConnectionInput: peerNum=${peerNum}, input="${input}"`);
-
- const setValidating =
- peerNum === 1 ? setPeerInputValidating1 : setPeerInputValidating2;
- const setError = peerNum === 1 ? setPeerInputError1 : setPeerInputError2;
-
- // Clear previous error
- setError('');
-
- // If input is empty, clear everything
- if (!input.trim()) {
- dbg(
- `handlePeerConnectionInput: peerNum=${peerNum}, clearing (empty input)`,
- );
- if (peerNum === 1) {
- setPeerNpub1('');
- setPeerDeviceName1('');
- setPeerNonce1('');
- setPeerConnectionDetails1('');
- } else {
- setPeerNpub2('');
- setPeerDeviceName2('');
- setPeerNonce2('');
- setPeerConnectionDetails2('');
- }
- return;
- }
-
- // Set validating state
- setValidating(true);
-
- // Small delay to show validation state
- await new Promise(resolve => setTimeout(resolve, 300));
-
- const parsed = await parseConnectionDetails(input);
-
- if (parsed) {
- dbg(
- `handlePeerConnectionInput: peerNum=${peerNum}, VALID - npub=${parsed.npub.substring(
- 0,
- 20,
- )}..., deviceName=${parsed.deviceName}`,
- );
-
- // Check for duplicate npubs
- const isDuplicateLocal = parsed.npub === localNpub;
- const isDuplicatePeer1 = peerNum !== 1 && parsed.npub === peerNpub1;
- const isDuplicatePeer2 = peerNum !== 2 && parsed.npub === peerNpub2;
-
- if (isDuplicateLocal || isDuplicatePeer1 || isDuplicatePeer2) {
- let duplicateMsg = 'This device is already connected.';
- if (isDuplicateLocal) {
- duplicateMsg = 'Cannot connect to your own device.';
- }
- dbg(
- `handlePeerConnectionInput: peerNum=${peerNum}, DUPLICATE - ${duplicateMsg}`,
- );
- setError(duplicateMsg);
- setValidating(false);
- // Clear the input text
- if (peerNum === 1) {
- setPeerConnectionDetails1('');
- setPeerNonce1('');
- } else {
- setPeerConnectionDetails2('');
- setPeerNonce2('');
- }
- return;
- }
-
- if (peerNum === 1) {
- setPeerNpub1(parsed.npub);
- setPeerDeviceName1(parsed.deviceName);
- setPeerNonce1(parsed.partialNonce);
- setPeerConnectionDetails1(input.trim());
- setPeerInputError1('');
- } else {
- setPeerNpub2(parsed.npub);
- setPeerDeviceName2(parsed.deviceName);
- setPeerNonce2(parsed.partialNonce);
- setPeerConnectionDetails2(input.trim());
- setPeerInputError2('');
- }
-
- HapticFeedback.light();
- } else {
- dbg(`handlePeerConnectionInput: peerNum=${peerNum}, INVALID`);
-
- // Check if it's a hex string
- const hexPattern = /^[0-9a-fA-F]{64}$/;
- const parts = input.trim().split(':');
- const firstPart = parts[0]?.trim() || '';
-
- let errorMsg = '';
- if (hexPattern.test(firstPart)) {
- errorMsg =
- 'Hex string detected. Please use npub format (npub1...). The connection details should show the npub, not a hex string.';
- } else if (!input.includes(':')) {
- errorMsg = 'Missing colon separator. Format: npub1...:DeviceName';
- } else if (!firstPart.startsWith('npub1')) {
- errorMsg =
- 'Invalid format. Expected: npub1...:DeviceName (npub must start with "npub1")';
- } else {
- errorMsg = 'Invalid format. Expected: npub1...:DeviceName';
- }
-
- dbg(`handlePeerConnectionInput: peerNum=${peerNum}, error="${errorMsg}"`);
- setError(errorMsg);
-
- // Clear the input text and peer data
- if (peerNum === 1) {
- setPeerNpub1('');
- setPeerDeviceName1('');
- setPeerNonce1('');
- setPeerConnectionDetails1('');
- } else {
- setPeerNpub2('');
- setPeerDeviceName2('');
- setPeerNonce2('');
- setPeerConnectionDetails2('');
- }
- }
-
- setValidating(false);
- };
-
- const generateKeygenSessionParams = async () => {
- try {
- // Collect all npubs and device names
- // IMPORTANT: Trim whitespace and ensure consistent format
- const allNpubs: string[] = [];
- const allDeviceNames: string[] = [];
-
- // Add local npub (trimmed)
- if (localNpub && localNpub.trim()) {
- allNpubs.push(localNpub.trim());
- }
- if (deviceName && deviceName.trim()) {
- allDeviceNames.push(deviceName.trim());
- }
-
- // Add peer 1 (trimmed)
- if (peerNpub1 && peerNpub1.trim()) {
- allNpubs.push(peerNpub1.trim());
- }
- if (peerDeviceName1 && peerDeviceName1.trim()) {
- allDeviceNames.push(peerDeviceName1.trim());
- }
-
- // Add peer 2 for trio (trimmed)
- if (isTrio && peerNpub2 && peerNpub2.trim()) {
- allNpubs.push(peerNpub2.trim());
- }
- if (isTrio && peerDeviceName2 && peerDeviceName2.trim()) {
- allDeviceNames.push(peerDeviceName2.trim());
- }
-
- // Validate we have the correct number of npubs
- const expectedNpubs = isTrio ? 3 : 2;
- if (allNpubs.length !== expectedNpubs) {
- dbg(
- `ERROR: Expected ${expectedNpubs} npubs for ${
- isTrio ? 'trio' : 'duo'
- } mode, but got ${allNpubs.length}`,
- );
- dbg(
- 'allNpubs:',
- allNpubs.map(n => n.substring(0, 20) + '...'),
- );
- dbg(
- 'localNpub:',
- localNpub ? localNpub.substring(0, 20) + '...' : 'MISSING',
- );
- dbg(
- 'peerNpub1:',
- peerNpub1 ? peerNpub1.substring(0, 20) + '...' : 'MISSING',
- );
- if (isTrio) {
- dbg(
- 'peerNpub2:',
- peerNpub2 ? peerNpub2.substring(0, 20) + '...' : 'MISSING',
- );
- dbg(
- 'peerDeviceName2:',
- peerDeviceName2 ? peerDeviceName2 : 'MISSING',
- );
- }
- dbg('isTrio:', isTrio);
- return; // Don't generate session params if we don't have all npubs
- }
-
- // Additional validation for trio mode: ensure all 3 npubs are unique
- if (isTrio && allNpubs.length === 3) {
- const uniqueNpubs = new Set(allNpubs);
- if (uniqueNpubs.size !== 3) {
- dbg('ERROR: Duplicate npubs detected in trio mode!');
- dbg('allNpubs:', allNpubs);
- return;
- }
- }
-
- // Sort alphabetically - CRITICAL: must be same order on all devices
- const npubsSorted = [...allNpubs].sort().join(',');
- const deviceNamesSorted = [...allDeviceNames].sort().join(',');
-
- // Collect all partial nonces (local + peers)
- const allPartialNonces: string[] = [];
- if (partialNonce) {
- allPartialNonces.push(partialNonce);
- }
- if (peerNonce1) {
- allPartialNonces.push(peerNonce1);
- }
- if (isTrio && peerNonce2) {
- allPartialNonces.push(peerNonce2);
- }
-
- // Sort nonces and join as CSV
- const fullNonce = [...allPartialNonces].sort().join(',');
-
- // Log the exact inputs for session ID calculation (for debugging)
- dbg('=== SESSION ID CALCULATION ===');
- dbg('Mode:', isTrio ? 'TRIO' : 'DUO');
- dbg(
- 'All npubs (before sort):',
- allNpubs.map(n => n.substring(0, 30) + '...'),
- );
- dbg(
- 'All npubs (sorted):',
- npubsSorted.split(',').map(n => n.substring(0, 30) + '...'),
- );
- dbg('npubsSorted (full):', npubsSorted);
- dbg('All device names (before sort):', allDeviceNames);
- dbg('deviceNamesSorted:', deviceNamesSorted);
- dbg('All partial nonces (before sort):', allPartialNonces);
- dbg('fullNonce (sorted, CSV):', fullNonce);
- dbg(
- 'Session ID input string:',
- `${npubsSorted},${deviceNamesSorted},${fullNonce}`,
- );
-
- // Generate session ID
- const sessionIDHash = await BBMTLibNativeModule.sha256(
- `${npubsSorted},${deviceNamesSorted},${fullNonce}`,
- );
- setSessionID(sessionIDHash);
-
- // Generate session key
- const sessionKeyHash = await BBMTLibNativeModule.sha256(
- `${npubsSorted},${sessionIDHash}`,
- );
- setSessionKey(sessionKeyHash);
-
- // Generate chaincode
- const chaincodeHash = await BBMTLibNativeModule.sha256(
- `${sessionIDHash},${sessionKeyHash}`,
- );
- setChaincode(chaincodeHash);
-
- dbg('Generated session params:', {
- sessionID: sessionIDHash.substring(0, 16) + '...',
- sessionKey: sessionKeyHash.substring(0, 16) + '...',
- chaincode: chaincodeHash.substring(0, 16) + '...',
- fullNonce: fullNonce,
- npubsCount: allNpubs.length,
- });
- dbg('=== END SESSION ID CALCULATION ===');
- } catch (error: any) {
- dbg('Error generating session params:', error);
- Alert.alert('Error', 'Failed to generate session parameters');
- }
- };
-
- const startKeygen = async () => {
- if (!canStartKeygen) return;
-
- setIsPairing(true);
- setProgress(0);
- setStatus('Starting key generation...');
-
- try {
- // Prepare parties npubs CSV (sorted)
- // IMPORTANT: Must use the same npubs and same sorting as session ID generation
- const allNpubs: string[] = [];
- if (localNpub && localNpub.trim()) {
- allNpubs.push(localNpub.trim());
- }
- if (peerNpub1 && peerNpub1.trim()) {
- allNpubs.push(peerNpub1.trim());
- }
- if (isTrio && peerNpub2 && peerNpub2.trim()) {
- allNpubs.push(peerNpub2.trim());
- }
-
- // Validate we have the correct number
- const expectedNpubs = isTrio ? 3 : 2;
- if (allNpubs.length !== expectedNpubs) {
- throw new Error(
- `Expected ${expectedNpubs} npubs for ${
- isTrio ? 'trio' : 'duo'
- } mode, but got ${allNpubs.length}`,
- );
- }
-
- // Sort alphabetically (same as session ID generation)
- const partiesNpubsCSV = allNpubs.sort().join(',');
-
- dbg('=== START KEYGEN ===');
- dbg('Mode:', isTrio ? 'TRIO' : 'DUO');
- dbg(
- 'localNpub:',
- localNpub ? localNpub.substring(0, 30) + '...' : 'MISSING',
- );
- dbg(
- 'partiesNpubsCSV (sorted, all npubs):',
- partiesNpubsCSV.split(',').map(n => n.substring(0, 30) + '...'),
- );
-
- // Calculate expected peers (all npubs except local)
- const expectedPeers = allNpubs.filter(n => {
- const trimmedN = n.trim();
- const trimmedLocal = localNpub?.trim() || '';
- return trimmedN !== trimmedLocal;
- });
- dbg(
- 'Expected peers (excluding self):',
- expectedPeers.map(n => n.substring(0, 30) + '...'),
- );
- dbg(
- 'Expected peer count:',
- expectedPeers.length,
- isTrio ? '(should be 2 for trio)' : '(should be 1 for duo)',
- );
-
- dbg('sessionID:', sessionID.substring(0, 16) + '...');
- dbg('sessionKey:', sessionKey.substring(0, 16) + '...');
-
- if (isTrio && expectedPeers.length !== 2) {
- dbg(
- '⚠️ WARNING: In trio mode, expected 2 peers but got',
- expectedPeers.length,
- );
- dbg(
- 'This device will wait for',
- expectedPeers.length,
- 'peers. Make sure all 3 devices have all npubs connected!',
- );
- }
-
- // Prepare relays CSV
- const relaysCSV = relays.join(',');
-
- // Save relays to cache
- await LocalCache.setItem('nostr_relays', relaysCSV);
-
- // Log detailed info for debugging trio mode
- dbg('Starting Nostr keygen with:', {
- relays: relaysCSV,
- parties: partiesNpubsCSV,
- sessionID: sessionID,
- ppmFile: ppmFile,
- localNsec: localNsec ? localNsec.substring(0, 20) + '...' : 'MISSING',
- partiesNpubsCSV: partiesNpubsCSV,
- sessionKey: sessionKey.substring(0, 16) + '...',
- chaincode: chaincode.substring(0, 16) + '...',
- });
-
- // Log which npubs will be sent to Go backend
- const allPartiesList = partiesNpubsCSV.split(',');
- dbg('=== GO BACKEND INPUT ===');
- dbg('partiesNpubsCSV (full):', partiesNpubsCSV);
- dbg(
- 'All parties count:',
- allPartiesList.length,
- isTrio ? '(should be 3 for trio)' : '(should be 2 for duo)',
- );
- dbg(
- 'All parties list:',
- allPartiesList.map((n, i) => `${i + 1}. ${n.substring(0, 30)}...`),
- );
- dbg(
- 'localNpub (will be excluded by Go backend):',
- localNpub ? localNpub.substring(0, 30) + '...' : 'MISSING',
- );
- dbg(
- 'Expected PeersNpub (after Go excludes localNpub):',
- expectedPeers.map((n, i) => `${i + 1}. ${n.substring(0, 30)}...`),
- );
- dbg(
- 'Go backend will wait for',
- expectedPeers.length,
- 'peers to publish "ready" events',
- );
- dbg('=== END GO BACKEND INPUT ===');
-
- // Call native module
- let keyshareJSON = await BBMTLibNativeModule.nostrMpcTssSetup(
- relaysCSV,
- localNsec,
- partiesNpubsCSV,
- sessionID,
- sessionKey,
- chaincode,
- ppmFile,
- );
-
- // Validate keyshare and map keyshare positions
- let keyshare: any;
- try {
- keyshare = JSON.parse(keyshareJSON);
- if (!keyshare.pub_key) {
- throw new Error('Invalid keyshare: missing pub_key');
- }
- dbg('Keygen successful, party:', keyshare.local_party_key);
- } catch (error) {
- dbg('Error parsing keyshare:', error);
- throw new Error('Invalid keyshare received');
- }
-
- // Map keyshare positions based on sorted npubs for UI display
- const sortedNpubs = allNpubs.sort();
- const mapping: {
- keyshare1?: {npub: string; deviceName: string; isLocal: boolean};
- keyshare2?: {npub: string; deviceName: string; isLocal: boolean};
- keyshare3?: {npub: string; deviceName: string; isLocal: boolean};
- } = {};
-
- // Map npubs to keyshare positions using keygen_committee_keys order
- // We need to match npubs to their corresponding hex keys in keygen_committee_keys
- // For now, we'll use the sorted npubs order which should match the sorted keygen_committee_keys
- sortedNpubs.forEach((npub, index) => {
- const isLocal = npub === localNpub;
- let mappedDeviceName = '';
-
- if (isLocal) {
- mappedDeviceName = deviceName || 'This device';
- } else if (npub === peerNpub1) {
- mappedDeviceName = peerDeviceName1 || 'Peer 1';
- } else if (npub === peerNpub2) {
- mappedDeviceName = peerDeviceName2 || 'Peer 2';
- } else {
- mappedDeviceName = `Device ${index + 1}`;
- }
-
- const keyshareKey = `keyshare${index + 1}` as
- | 'keyshare1'
- | 'keyshare2'
- | 'keyshare3';
- mapping[keyshareKey] = {
- npub,
- deviceName: mappedDeviceName,
- isLocal,
- };
- });
-
- setKeyshareMapping(mapping);
- dbg('Keyshare mapping:', mapping);
-
- // Save keyshare (keyshare_position will be calculated on-the-fly when needed)
- await EncryptedStorage.setItem('keyshare', keyshareJSON);
- setMpcDone(true);
- setStatus('Key generation complete!');
- // Don't navigate away, let the backup UI handle it
- } catch (error: any) {
- dbg('Keygen error:', error);
- Alert.alert('Error', error?.message || 'Key generation failed');
- setStatus('Key generation failed');
- // Navigate to index 0 (reload same page) on keygen failure
- navigation.dispatch(
- CommonActions.reset({
- index: 0,
- routes: [{name: 'Nostr Connect', params: route.params}],
- }),
- );
- } finally {
- setIsPairing(false);
- }
- };
-
- const startSendBTC = async () => {
- if (!route.params) {
- Alert.alert('Error', 'Missing transaction parameters');
- return;
- }
-
- setIsPairing(true);
- setProgress(0);
- setStatus('Starting transaction signing...');
-
- try {
- // Get wallet balance in satoshis
- const keyshareJSON = await EncryptedStorage.getItem('keyshare');
- if (!keyshareJSON) {
- throw new Error('Keyshare not found');
- }
-
- const keyshare = JSON.parse(keyshareJSON);
-
- // Get nsec from keyshare (use local variable, not state)
- let nsecToUse = localNsec;
- if (!nsecToUse || !nsecToUse.startsWith('nsec1')) {
- const nsecFromKeyshare = keyshare.nsec || '';
- if (nsecFromKeyshare) {
- try {
- let decodedNsec = '';
-
- // Check if it's already in bech32 format
- if (nsecFromKeyshare.startsWith('nsec1')) {
- decodedNsec = nsecFromKeyshare;
- } else {
- decodedNsec = hexToString(nsecFromKeyshare);
- }
-
- if (decodedNsec.startsWith('nsec1')) {
- nsecToUse = decodedNsec;
- setLocalNsec(decodedNsec); // Update state for UI
- dbg(
- 'Loaded nsec from keyshare in startSendBTC:',
- decodedNsec.substring(0, 20) + '...',
- );
- } else {
- throw new Error(
- `Invalid nsec format in keyshare: ${decodedNsec.substring(
- 0,
- 50,
- )}`,
- );
- }
- } catch (error) {
- dbg('Error loading nsec from keyshare in startSendBTC:', error);
- throw new Error(`Failed to load nsec from keyshare: ${error}`);
- }
- } else {
- throw new Error('nsec not found in keyshare');
- }
- }
-
- // Verify nsec is valid
- if (!nsecToUse || !nsecToUse.startsWith('nsec1')) {
- throw new Error(
- 'Invalid nsec: nsec must be in bech32 format (nsec1...)',
- );
- }
-
- if (!activeAddress) {
- throw new Error('Active address not found');
- }
-
- const balance = await WalletService.getInstance().getWalletBalance(
- activeAddress,
- 0,
- 0,
- false,
- );
- const balanceSats = Big(balance.btc).times(1e8).toString();
-
- // IMPORTANT: For session ID, we need ALL npubs from the keyshare (all participants)
- // Get all hex keys from keygen_committee_keys and convert them ALL to npubs
- const allNpubsFromKeyshare: string[] = [];
-
- // Get all keys from keygen_committee_keys and convert them ALL to npubs
- const sortedKeys = [...keyshare.keygen_committee_keys].sort();
- for (const key of sortedKeys) {
- try {
- // Check if it's already an npub (shouldn't happen, but handle it)
- if (key && typeof key === 'string' && key.startsWith('npub1')) {
- allNpubsFromKeyshare.push(key);
- dbg('Key already npub format:', key.substring(0, 20) + '...');
- continue;
- }
-
- // Validate hex key format
- const hexPattern = /^[0-9a-fA-F]+$/;
- if (!hexPattern.test(key)) {
- dbg(
- 'Invalid key format (not hex, not npub), skipping:',
- key.substring(0, 20) + '...',
- );
- continue;
- }
-
- // Convert hex to npub (convert ALL keys, including local)
- const npub = await BBMTLibNativeModule.hexToNpub(key);
- if (npub && typeof npub === 'string' && npub.startsWith('npub1')) {
- allNpubsFromKeyshare.push(npub);
- dbg(
- 'Converted hex to npub for session ID:',
- npub.substring(0, 20) + '...',
- );
- } else {
- dbg('Failed to convert hex to npub, result:', npub);
- }
- } catch (error) {
- dbg('Error converting hex to npub for session ID:', error);
- }
- }
-
- // Sort all npubs - this must match on all devices
- const npubsSorted = [...allNpubsFromKeyshare].sort().join(',');
-
- if (npubsSorted.length === 0 || allNpubsFromKeyshare.length < 2) {
- throw new Error(
- `Failed to get all npubs from keyshare. Got ${allNpubsFromKeyshare.length} npubs. Please ensure all devices are loaded.`,
- );
- }
-
- dbg(
- 'All npubs for session ID:',
- allNpubsFromKeyshare.map(n => n.substring(0, 20) + '...'),
- );
-
- // Prepare parties npubs CSV for the actual signing (only participating devices)
- // IMPORTANT: Use the full npubs from allNpubsFromKeyshare (already converted from hex)
- // This ensures we use the same npubs that were used for session ID calculation
- // Find local npub in allNpubsFromKeyshare to ensure consistency
- const localNpubFromKeyshare =
- allNpubsFromKeyshare.find(n => {
- // Match by checking if localNpub (from state) matches or starts with this npub
- return (
- n === localNpub ||
- (localNpub && n.startsWith(localNpub.substring(0, 20)))
- );
- }) || localNpub; // Fallback to state if not found
-
- const allNpubs = [localNpubFromKeyshare];
- if (isTrio) {
- // For trio, use selected peer - find it by matching device in sendModeDevices
- if (selectedPeerNpub) {
- // Find the selected device in sendModeDevices to get its keyshareLabel
- const selectedDevice = sendModeDevices.find(
- d =>
- d.npub === selectedPeerNpub ||
- (selectedPeerNpub.startsWith('npub1') &&
- d.npub &&
- d.npub.startsWith(selectedPeerNpub.substring(0, 20))) ||
- (d.npub && selectedPeerNpub.startsWith(d.npub.substring(0, 20))),
- );
-
- if (selectedDevice) {
- // Find the corresponding hex key in keyshare by keyshareLabel
- // Use the same sortedKeys from above (already sorted)
- const selectedIndex =
- parseInt(
- selectedDevice.keyshareLabel.replace('KeyShare', ''),
- 10,
- ) - 1;
-
- if (selectedIndex >= 0 && selectedIndex < sortedKeys.length) {
- const selectedHexKey = sortedKeys[selectedIndex];
-
- // Find the full npub in allNpubsFromKeyshare that corresponds to this hex key
- // We need to convert the hex key to npub and find it, or match by index
- // Since allNpubsFromKeyshare is built from sortedKeys in the same order, we can use index
- if (selectedIndex < allNpubsFromKeyshare.length) {
- const fullPeerNpub = allNpubsFromKeyshare[selectedIndex];
- // Verify it's not the local device
- if (fullPeerNpub !== localNpubFromKeyshare) {
- allNpubs.push(fullPeerNpub);
- dbg(
- 'Found full peer npub for trio by index:',
- fullPeerNpub.substring(0, 20) + '...',
- );
- } else {
- throw new Error('Selected device is the local device');
- }
- } else {
- // Fallback: try to convert hex key to npub
- try {
- const hexPattern = /^[0-9a-fA-F]+$/;
- if (hexPattern.test(selectedHexKey)) {
- const convertedNpub = await BBMTLibNativeModule.hexToNpub(
- selectedHexKey,
- );
- if (
- convertedNpub &&
- convertedNpub.startsWith('npub1') &&
- convertedNpub !== localNpubFromKeyshare
- ) {
- allNpubs.push(convertedNpub);
- dbg(
- 'Found full peer npub for trio by conversion:',
- convertedNpub.substring(0, 20) + '...',
- );
- } else {
- throw new Error(
- 'Failed to convert selected hex key to npub',
- );
- }
- } else {
- throw new Error('Selected hex key is not valid hex');
- }
- } catch (error) {
- throw new Error(
- `Failed to find full npub for selected peer: ${error}`,
- );
- }
- }
- } else {
- throw new Error(
- `Invalid keyshare label: ${selectedDevice.keyshareLabel}`,
- );
- }
- } else {
- // Fallback: try direct matching in allNpubsFromKeyshare
- let fullPeerNpub = allNpubsFromKeyshare.find(
- n =>
- n === selectedPeerNpub ||
- (selectedPeerNpub.startsWith('npub1') &&
- n.startsWith(selectedPeerNpub.substring(0, 20))),
- );
-
- if (fullPeerNpub && fullPeerNpub !== localNpubFromKeyshare) {
- allNpubs.push(fullPeerNpub);
- dbg(
- 'Found full peer npub for trio by direct match:',
- fullPeerNpub.substring(0, 20) + '...',
- );
- } else {
- throw new Error(
- `Failed to find full npub for selected peer: ${selectedPeerNpub.substring(
- 0,
- 30,
- )}. Please ensure the device is fully loaded.`,
- );
- }
- }
- } else {
- throw new Error('Please select a peer device for trio mode');
- }
- } else {
- // For duo, use the other device from allNpubsFromKeyshare (excluding local)
- const otherNpubs = allNpubsFromKeyshare.filter(
- n => n !== localNpubFromKeyshare,
- );
- if (otherNpubs.length > 0) {
- // In duo mode, there should be exactly one other npub
- allNpubs.push(otherNpubs[0]);
- dbg(
- 'Using other npub for duo:',
- otherNpubs[0].substring(0, 20) + '...',
- );
- } else {
- throw new Error('Other device npub not found in keyshare');
- }
- }
- const partiesNpubsCSV = allNpubs.sort().join(',');
-
- dbg(
- 'partiesNpubsCSV for signing (full npubs, length=',
- partiesNpubsCSV.length,
- '):',
- partiesNpubsCSV.substring(0, 100) + '...',
- );
- const satoshiAmount = route.params.satoshiAmount || '0';
- const satoshiFees = route.params.satoshiFees || '0';
-
- // Generate session ID (includes all transaction details that must match)
- // Format: sha256(npubsSorted,balance,amount,rounded)
- const rounded = Math.floor(Date.now() / 90000);
-
- dbg(
- 'session id params:',
- `${npubsSorted},${balanceSats},${satoshiAmount},${rounded}`,
- );
- dbg(
- 'session_params',
- JSON.stringify(
- {npubsSorted, balanceSats, satoshiAmount, rounded},
- null,
- 4,
- ),
- );
-
- // Prepare relays CSV
- const relaysCSV = relays.join(',');
-
- const network = (await LocalCache.getItem('network')) || 'mainnet';
- const derivePath = getDerivePathForNetwork(network);
-
- // Derive the public key from the root key using the derivation path
- // This is critical - we need the DERIVED public key, not the root!
- const publicKey = await BBMTLibNativeModule.derivePubkey(
- keyshare.pub_key,
- keyshare.chain_code_hex,
- derivePath,
- );
-
- dbg(
- 'Derived public key for path:',
- derivePath,
- 'pubKey:',
- publicKey.substring(0, 20) + '...',
- );
-
- // Generate BTC address using addressType (same as MobilesPairing.tsx)
- const net = (await LocalCache.getItem('network')) || 'mainnet';
- const btcAddress = await BBMTLibNativeModule.btcAddress(
- publicKey,
- net,
- addressType,
- );
-
- dbg('Starting Nostr send BTC with:', {
- relays: relaysCSV,
- parties: partiesNpubsCSV,
- npubsSorted: npubsSorted.substring(0, 30) + '...',
- balance: balanceSats,
- amount: route.params?.satoshiAmount,
- localNsec: nsecToUse ? nsecToUse.substring(0, 20) + '...' : 'MISSING',
- derivePath: derivePath,
- derivedPublicKey: publicKey.substring(0, 20) + '...',
- btcAddress: btcAddress,
- addressType: addressType,
- estimatedFees: satoshiFees,
- });
-
- dbg(
- 'Calling nostrMpcSendBTC (pre-agreement handled internally):',
- nsecToUse.substring(0, 20) + '...',
- );
-
- // Call native module - pre-agreement is now handled internally in Go
- // The function will calculate sessionFlag, do pre-agreement, update sessionID and fees
- // Use btcAddress generated from addressType (same as MobilesPairing.tsx)
- const txId = await BBMTLibNativeModule.nostrMpcSendBTC(
- relaysCSV,
- nsecToUse,
- partiesNpubsCSV,
- npubsSorted,
- balanceSats,
- keyshareJSON,
- derivePath,
- publicKey,
- btcAddress,
- route.params.toAddress || '',
- route.params.satoshiAmount || '0',
- route.params.satoshiFees || '0',
- );
-
- // Validate txId
- const validTxID = /^[a-fA-F0-9]{64}$/.test(txId);
- if (!validTxID) {
- throw new Error(txId || 'Invalid transaction ID');
- }
-
- // Save pending transaction
- const pendingTxs = JSON.parse(
- (await LocalCache.getItem(`${activeAddress}-pendingTxs`)) || '{}',
- );
- pendingTxs[txId] = {
- txid: txId,
- from: activeAddress,
- to: route.params.toAddress,
- amount: route.params.satoshiAmount,
- satoshiAmount: route.params.satoshiAmount,
- satoshiFees: route.params.satoshiFees,
- sentAt: Date.now(),
- status: {
- confirmed: false,
- block_height: null,
- },
- };
- await LocalCache.setItem(
- `${activeAddress}-pendingTxs`,
- JSON.stringify(pendingTxs),
- );
-
- // Navigate to home (same as MobilesPairing.tsx)
- navigation.dispatch(
- CommonActions.reset({
- index: 0,
- routes: [{name: 'Home', params: {txId}}],
- }),
- );
-
- setMpcDone(true);
- } catch (error: any) {
- dbg('Send BTC error:', error);
- Alert.alert('Error', error?.message || 'Transaction signing failed');
- setStatus('Transaction signing failed');
- } finally {
- setIsPairing(false);
- }
- };
-
- // PSBT Signing function - similar to startSendBTC but for PSBT
- const startSignPSBT = async () => {
- if (!route.params?.psbtBase64) {
- Alert.alert('Error', 'Missing PSBT data');
- return;
- }
-
- if (!isKeysignReady) {
- Alert.alert('Not Ready', 'Please confirm that you are ready to sign the PSBT');
- return;
- }
-
- setIsPairing(true);
- setProgress(0);
- setStatus('Starting PSBT signing...');
-
- try {
- const keyshareJSON = await EncryptedStorage.getItem('keyshare');
- if (!keyshareJSON) {
- throw new Error('Keyshare not found');
- }
-
- const keyshare = JSON.parse(keyshareJSON);
-
- // Get nsec from keyshare
- let nsecToUse = localNsec;
- if (!nsecToUse || !nsecToUse.startsWith('nsec1')) {
- const nsecFromKeyshare = keyshare.nsec || '';
- if (nsecFromKeyshare) {
- try {
- let decodedNsec = '';
- if (nsecFromKeyshare.startsWith('nsec1')) {
- decodedNsec = nsecFromKeyshare;
- } else {
- decodedNsec = hexToString(nsecFromKeyshare);
- }
-
- if (decodedNsec.startsWith('nsec1')) {
- nsecToUse = decodedNsec;
- setLocalNsec(decodedNsec);
- } else {
- throw new Error('Invalid nsec format in keyshare');
- }
- } catch (error) {
- throw new Error(`Failed to load nsec from keyshare: ${error}`);
- }
- } else {
- throw new Error('nsec not found in keyshare');
- }
- }
-
- if (!nsecToUse || !nsecToUse.startsWith('nsec1')) {
- throw new Error('Invalid nsec: must be in bech32 format (nsec1...)');
- }
-
- // Get all npubs from keyshare for session ID
- const allNpubsFromKeyshare: string[] = [];
- const sortedKeys = [...keyshare.keygen_committee_keys].sort();
- for (const key of sortedKeys) {
- try {
- if (key && typeof key === 'string' && key.startsWith('npub1')) {
- allNpubsFromKeyshare.push(key);
- continue;
- }
- const hexPattern = /^[0-9a-fA-F]+$/;
- if (!hexPattern.test(key)) {
- continue;
- }
- const npub = await BBMTLibNativeModule.hexToNpub(key);
- if (npub && typeof npub === 'string' && npub.startsWith('npub1')) {
- allNpubsFromKeyshare.push(npub);
- }
- } catch (error) {
- dbg('Error converting hex to npub:', error);
- }
- }
-
- const npubsSorted = [...allNpubsFromKeyshare].sort().join(',');
-
- if (npubsSorted.length === 0 || allNpubsFromKeyshare.length < 2) {
- throw new Error('Failed to get all npubs from keyshare');
- }
-
- // Find local npub
- const localNpubFromKeyshare =
- allNpubsFromKeyshare.find(n => n === localNpub || (localNpub && n.startsWith(localNpub.substring(0, 20)))) ||
- localNpub;
-
- // Build parties CSV
- const allNpubs = [localNpubFromKeyshare];
- if (isTrio) {
- if (selectedPeerNpub) {
- const selectedDevice = sendModeDevices.find(
- d => d.npub === selectedPeerNpub || (selectedPeerNpub.startsWith('npub1') && d.npub && d.npub.startsWith(selectedPeerNpub.substring(0, 20))),
- );
- if (selectedDevice) {
- const selectedIndex = parseInt(selectedDevice.keyshareLabel.replace('KeyShare', ''), 10) - 1;
- if (selectedIndex >= 0 && selectedIndex < allNpubsFromKeyshare.length) {
- const fullPeerNpub = allNpubsFromKeyshare[selectedIndex];
- if (fullPeerNpub !== localNpubFromKeyshare) {
- allNpubs.push(fullPeerNpub);
- } else {
- throw new Error('Selected device is the local device');
- }
- }
- } else {
- const fullPeerNpub = allNpubsFromKeyshare.find(
- n => n === selectedPeerNpub || (selectedPeerNpub.startsWith('npub1') && n.startsWith(selectedPeerNpub.substring(0, 20))),
- );
- if (fullPeerNpub && fullPeerNpub !== localNpubFromKeyshare) {
- allNpubs.push(fullPeerNpub);
- } else {
- throw new Error('Failed to find full npub for selected peer');
- }
- }
- } else {
- throw new Error('Please select a peer device for trio mode');
- }
- } else {
- const otherNpubs = allNpubsFromKeyshare.filter(n => n !== localNpubFromKeyshare);
- if (otherNpubs.length > 0) {
- allNpubs.push(otherNpubs[0]);
- } else {
- throw new Error('Other device npub not found in keyshare');
- }
- }
- const partiesNpubsCSV = allNpubs.sort().join(',');
-
- const relaysCSV = relays.join(',');
- const network = (await LocalCache.getItem('network')) || 'mainnet';
- const derivePath = route.params?.derivePath || getDerivePathForNetwork(network);
-
- dbg('Using derivation path for PSBT signing:', derivePath);
-
- // Derive the public key
- const publicKey = await BBMTLibNativeModule.derivePubkey(
- keyshare.pub_key,
- keyshare.chain_code_hex,
- derivePath,
- );
-
- dbg('Starting Nostr PSBT signing with:', {
- relays: relaysCSV,
- parties: partiesNpubsCSV.substring(0, 50) + '...',
- npubsSorted: npubsSorted.substring(0, 30) + '...',
- derivePath,
- publicKey: publicKey.substring(0, 20) + '...',
- psbtLength: route.params.psbtBase64?.length,
- });
-
- // Call native module for PSBT signing
- const signedPsbt = await BBMTLibNativeModule.nostrMpcSignPSBT(
- relaysCSV,
- nsecToUse,
- partiesNpubsCSV,
- npubsSorted,
- keyshareJSON,
- route.params.psbtBase64,
- );
-
- // Validate result
- if (!signedPsbt || signedPsbt.includes('error') || signedPsbt.includes('failed')) {
- throw new Error(signedPsbt || 'PSBT signing failed');
- }
-
- dbg('PSBT signed successfully, length:', signedPsbt.length);
-
- // Check user's wallet mode preference before navigating
- let targetRoute = 'Home';
- try {
- const walletMode =
- (await EncryptedStorage.getItem('wallet_mode')) || 'full';
- targetRoute = walletMode === 'psbt' ? 'PSBT' : 'Home';
- dbg(
- 'PSBT signing complete: Navigating to',
- targetRoute,
- 'based on wallet_mode:',
- walletMode,
- );
- } catch (error) {
- dbg('Error loading wallet_mode after PSBT signing:', error);
- // Default to 'Home' if there's an error
- }
-
- // Navigate to the appropriate screen based on user preference
- navigation.dispatch(
- CommonActions.reset({
- index: 0,
- routes: [{name: targetRoute, params: {signedPsbt}}],
- }),
- );
-
- setMpcDone(true);
- } catch (error: any) {
- dbg('Sign PSBT error:', error);
- Alert.alert('Error', error?.message || 'PSBT signing failed');
- setStatus('PSBT signing failed');
- } finally {
- setIsPairing(false);
- }
- };
-
- // Backup functions
- const allBackupChecked = isTrio
- ? backupChecks.deviceOne &&
- backupChecks.deviceTwo &&
- backupChecks.deviceThree
- : backupChecks.deviceOne && backupChecks.deviceTwo;
-
- const toggleBackedup = (key: keyof typeof backupChecks) => {
- setBackupChecks(prev => ({...prev, [key]: !prev[key]}));
- };
-
- const formatFiat = (price?: string) =>
- new Intl.NumberFormat('en-US', {
- style: 'decimal',
- minimumFractionDigits: 2,
- maximumFractionDigits: 2,
- }).format(Number(price));
-
- const sat2btcStr = (sats?: string) =>
- Big(sats || 0)
- .div(1e8)
- .toFixed(8);
-
- const validatePassword = (pass: string) => {
- const errors: string[] = [];
- const rules = {
- length: pass.length >= 12,
- uppercase: /[A-Z]/.test(pass),
- lowercase: /[a-z]/.test(pass),
- number: /\d/.test(pass),
- symbol: /[!@#$%^&*(),.?":{}|<>]/.test(pass),
- };
-
- if (!rules.length) {
- errors.push('12+ characters');
- }
- if (!rules.uppercase) {
- errors.push('Uppercase letter (A-Z)');
- }
- if (!rules.lowercase) {
- errors.push('Lowercase letter (a-z)');
- }
- if (!rules.number) {
- errors.push('Number (0-9)');
- }
- if (!rules.symbol) {
- errors.push('Special character (!@#$...)');
- }
- setPasswordErrors(errors);
-
- // Calculate strength (0-4)
- const strength = Object.values(rules).filter(Boolean).length;
- setPasswordStrength(strength);
-
- return errors.length === 0;
- };
-
- const handlePasswordChange = (text: string) => {
- setPassword(text);
- validatePassword(text);
- };
-
- const getPasswordStrengthColor = () => {
- if (passwordStrength <= 1) {
- return theme.colors.danger;
- }
- if (passwordStrength <= 2) {
- return '#FFA500';
- }
- if (passwordStrength <= 3) {
- return '#FFD700';
- }
- return '#4CAF50';
- };
-
- const getPasswordStrengthText = () => {
- if (passwordStrength <= 1) {
- return 'Very Weak';
- }
- if (passwordStrength <= 2) {
- return 'Weak';
- }
- if (passwordStrength <= 3) {
- return 'Medium';
- }
- return 'Strong';
- };
-
- const clearBackupModal = () => {
- setPassword('');
- setConfirmPassword('');
- setPasswordVisible(false);
- setConfirmPasswordVisible(false);
- setPasswordStrength(0);
- setPasswordErrors([]);
- setIsBackupModalVisible(false);
- };
-
- async function backupShare() {
- if (!validatePassword(password)) {
- dbg('❌ [BACKUP] Password validation failed');
- const missingRequirements = passwordErrors.join('\n• ');
- Alert.alert(
- 'Password Requirements Not Met',
- `Your password must meet all of the following requirements:\n\n• ${missingRequirements}\n\nPlease update your password and try again.`,
- );
- return;
- }
-
- if (password !== confirmPassword) {
- dbg('❌ [BACKUP] Password mismatch');
- Alert.alert(
- 'Passwords Do Not Match',
- 'The password and confirmation password must be identical. Please check both fields and try again.',
- );
- return;
- }
-
- try {
- HapticFeedback.medium();
-
- const storedKeyshare = await EncryptedStorage.getItem('keyshare');
- if (storedKeyshare) {
- const json = JSON.parse(storedKeyshare);
- const encryptedKeyshare = await BBMTLibNativeModule.aesEncrypt(
- storedKeyshare,
- await BBMTLibNativeModule.sha256(password),
- );
-
- // Create friendly filename with date and time
- const now = new Date();
- const month = now.toLocaleDateString('en-US', {month: 'short'});
- const day = now.getDate().toString().padStart(2, '0');
- const year = now.getFullYear();
- const hours = now.getHours().toString().padStart(2, '0');
- const minutes = now.getMinutes().toString().padStart(2, '0');
- // Use keyshare label (KeyShare1/2/3) or fallback to local_party_key
- const keyshareLabel = getKeyshareLabel(json);
- const shareName = keyshareLabel || json.local_party_key || 'keyshare';
- const friendlyFilename = `${shareName}.${month}${day}.${year}.${hours}${minutes}.share`;
-
- const tempDir = RNFS.TemporaryDirectoryPath || RNFS.CachesDirectoryPath;
- const filePath = `${tempDir}/${friendlyFilename}`;
-
- await RNFS.writeFile(filePath, encryptedKeyshare, 'base64');
-
- await Share.open({
- title: 'Backup Your Keyshare',
- message:
- 'Save this encrypted file securely. It is required for wallet recovery.',
- url: `file://${filePath}`,
- type: 'application/octet-stream',
- filename: friendlyFilename,
- failOnCancel: false,
- });
-
- try {
- await RNFS.unlink(filePath);
- } catch {}
- clearBackupModal();
- } else {
- Alert.alert('Error', 'Invalid keyshare.');
- }
- } catch (error) {
- dbg('Error encrypting or sharing keyshare:', error);
- Alert.alert('Error', 'Failed to encrypt or share the keyshare.');
- }
- }
-
- const copyConnectionDetails = () => {
- Clipboard.setString(connectionDetails);
- HapticFeedback.medium();
- Alert.alert(
- 'Copied',
- '- Pairing data copied.\n- Paste them to other device(s)',
- );
- };
-
- const shareConnectionDetails = async () => {
- HapticFeedback.medium();
-
- if (!connectionDetails) {
- Alert.alert('Error', 'Connection details are not ready yet');
- return;
- }
-
- if (!connectionQrRef.current) {
- Alert.alert('Error', 'QR Code is not ready yet');
- return;
- }
-
- try {
- // Generate base64 from QR component (similar to WalletHome ReceiveModal)
- const base64Data: string = await new Promise((resolve, reject) => {
- connectionQrRef.current.toDataURL((data: string) => {
- if (data) {
- resolve(data);
- } else {
- reject(new Error('No base64 data returned from QR code'));
- }
- });
- });
-
- const filePath = `${RNFS.TemporaryDirectoryPath}/boldwallet-connection-details.jpg`;
- const fileExists = await RNFS.exists(filePath);
- if (fileExists) {
- await RNFS.unlink(filePath);
- }
-
- await RNFS.writeFile(filePath, base64Data, 'base64');
-
- await Share.open({
- title: 'Bold Wallet Connection Details',
- message: connectionDetails,
- url: `file://${filePath}`,
- subject: 'Bold Wallet Connection Details',
- isNewTask: true,
- failOnCancel: false,
- });
-
- // Best-effort cleanup
- await RNFS.unlink(filePath).catch(() => {});
-
- setIsQRModalVisible(false);
- } catch (error: any) {
- dbg('Error sharing connection details (QR + text):', error);
- Alert.alert('Error', 'Failed to share connection QR code');
- }
- };
-
- const showQRModal = () => {
- HapticFeedback.medium();
- setIsQRModalVisible(true);
- };
-
- const handleQRScan = (data: string, peerNum?: 1 | 2) => {
- HapticFeedback.medium();
- // FOSS version: No need to close iOS scanner modal (removed)
- // Use provided peerNum, or fallback to scanningForPeerRef (more reliable than state)
- const targetPeer = peerNum || scanningForPeerRef.current;
- dbg(
- `handleQRScan: data="${data.substring(
- 0,
- 30,
- )}...", peerNum=${targetPeer}, scanningForPeerRef=${
- scanningForPeerRef.current
- }`,
- );
- handlePeerConnectionInput(data, targetPeer);
- };
-
- const handlePaste = async (peerNum: 1 | 2) => {
- try {
- const text = await Clipboard.getString();
- dbg(`handlePaste: peerNum=${peerNum}, pasted text="${text}"`);
- HapticFeedback.medium();
-
- // Update the input field immediately so user can see what was pasted
- if (peerNum === 1) {
- setPeerConnectionDetails1(text);
- } else {
- setPeerConnectionDetails2(text);
- }
-
- // Then validate the input
- await handlePeerConnectionInput(text, peerNum);
- } catch (error) {
- dbg('Error pasting:', error);
- Alert.alert('Error', 'Failed to paste from clipboard');
- }
- };
-
- const clearPeerConnection = (peerNum: 1 | 2) => {
- HapticFeedback.medium();
- if (peerNum === 1) {
- setPeerNpub1('');
- setPeerDeviceName1('');
- setPeerNonce1('');
- setPeerConnectionDetails1('');
- setPeerInputError1('');
- } else {
- setPeerNpub2('');
- setPeerDeviceName2('');
- setPeerNonce2('');
- setPeerConnectionDetails2('');
- setPeerInputError2('');
- }
- };
-
- const deletePreparams = async () => {
- try {
- dbg(`deleting ppmFile: ${ppmFile}`);
- await RNFS.unlink(ppmFile);
- dbg('ppmFile deleted');
- } catch (err: any) {
- dbg('error deleting ppmFile', err);
- }
- };
-
- const prepareDevice = async () => {
- setIsPreparing(true);
- setIsPreParamsReady(false);
- setPrepCounter(0);
-
- const timeoutMinutes = 20;
-
- if (!__DEV__) {
- await deletePreparams();
- } else {
- dbg('preparams dev: Not deleting ppmFile');
- }
-
- try {
- await BBMTLibNativeModule.preparams(ppmFile, String(timeoutMinutes));
- setIsPreParamsReady(true);
- HapticFeedback.medium();
- dbg('Device prepared successfully');
- } catch (error: any) {
- setIsPreParamsReady(false);
- dbg('Error preparing device:', error);
- Alert.alert('Error', error?.toString() || 'Failed to prepare device');
- } finally {
- setIsPreparing(false);
- setPrepCounter(0);
- }
- };
-
- // Increment prep counter when preparing
- useEffect(() => {
- if (isPreparing) {
- const interval = setInterval(() => {
- setPrepCounter(prevCounter => prevCounter + 1);
- }, 1000);
- return () => clearInterval(interval);
- }
- }, [isPreparing]);
-
- // Track elapsed time during keygen and signing
- useEffect(() => {
- if (isPairing) {
- setPrepCounter(0);
- const interval = setInterval(() => {
- setPrepCounter(prevCounter => prevCounter + 1);
- }, 1000);
- return () => clearInterval(interval);
- }
- }, [isPairing]);
-
- // Animation for horizontal progress bar
- useEffect(() => {
- if (isPreparing) {
- // Stop any existing animation first
- if (progressAnimationLoop.current) {
- progressAnimationLoop.current.stop();
- progressAnimationLoop.current = null;
- }
-
- // Small delay to ensure modal is mounted before starting animation
- const timeoutId = setTimeout(() => {
- // Reset value before starting new animation (only when modal is mounted)
- progressAnimation.setValue(0);
-
- // Start new animation loop
- progressAnimationLoop.current = Animated.loop(
- Animated.sequence([
- Animated.timing(progressAnimation, {
- toValue: 1,
- duration: 2000,
- useNativeDriver: false,
- }),
- Animated.timing(progressAnimation, {
- toValue: 0,
- duration: 2000,
- useNativeDriver: false,
- }),
- ]),
- );
- progressAnimationLoop.current.start();
- }, 150);
-
- return () => {
- clearTimeout(timeoutId);
- if (progressAnimationLoop.current) {
- progressAnimationLoop.current.stop();
- progressAnimationLoop.current = null;
- }
- // Stop any running animation without setting value
- progressAnimation.stopAnimation();
- };
- } else {
- // Stop animation without setting value to avoid warning
- if (progressAnimationLoop.current) {
- progressAnimationLoop.current.stop();
- progressAnimationLoop.current = null;
- }
- // Stop any running animation
- progressAnimation.stopAnimation();
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [isPreparing]);
-
- // Styles
- const styles = StyleSheet.create({
- container: {
- flex: 1,
- backgroundColor: theme.colors.background,
- },
- scrollView: {
- flex: 1,
- },
- content: {
- padding: 20,
- },
- section: {
- marginBottom: 8,
- },
- sectionTitle: {
- fontSize: 18,
- fontWeight: '700',
- color: theme.colors.text,
- marginBottom: 12,
- },
- card: {
- backgroundColor: theme.colors.cardBackground,
- borderRadius: 12,
- padding: 4,
- borderWidth: 1,
- borderColor: theme.colors.border + '40',
- },
- cardSelected: {
- borderColor: theme.colors.primary,
- borderWidth: 2,
- backgroundColor: theme.colors.primary + '10',
- },
- deviceInfoRowWithCheckbox: {
- flexDirection: 'row',
- alignItems: 'center',
- justifyContent: 'space-between',
- marginBottom: 12,
- paddingVertical: 8,
- },
- peerCheckbox: {
- width: 24,
- height: 24,
- borderRadius: 6,
- borderWidth: 2,
- borderColor: theme.colors.border,
- backgroundColor: theme.colors.cardBackground,
- alignItems: 'center',
- justifyContent: 'center',
- marginLeft: 12,
- },
- peerCheckboxChecked: {
- borderColor: theme.colors.primary,
- backgroundColor: theme.colors.primary,
- },
- peerCheckmark: {
- color: theme.colors.white,
- fontSize: 16,
- fontWeight: '700',
- },
- input: {
- borderWidth: 1.5,
- borderColor: theme.colors.border + '40',
- borderRadius: 12,
- paddingHorizontal: 16,
- paddingVertical: 14,
- fontSize: 16,
- color: theme.colors.text,
- backgroundColor: 'rgba(0,0,0,0.02)',
- },
- inputFocused: {
- borderColor: theme.colors.primary,
- backgroundColor: 'rgba(0,0,0,0.03)',
- },
- inputWithIcons: {
- flexDirection: 'row',
- alignItems: 'center',
- gap: 8,
- },
- inputFlex: {
- flex: 1,
- },
- inputCentered: {
- textAlignVertical: 'center',
- },
- inputTextDisplay: {
- paddingVertical: 14,
- lineHeight: 20,
- },
- iconButton: {
- width: 48,
- height: 48,
- borderRadius: 12,
- backgroundColor: theme.colors.primary + '20',
- alignItems: 'center',
- justifyContent: 'center',
- },
- iconButtonCentered: {
- alignSelf: 'center',
- },
- deviceInfoRow: {
- flexDirection: 'row',
- alignItems: 'center',
- marginBottom: 8,
- paddingVertical: 4,
- },
- deviceInfoSingleLine: {
- fontSize: 14,
- fontWeight: '600',
- fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace',
- color: theme.colors.text,
- flex: 1,
- textAlign: 'center',
- overflow: 'hidden',
- },
- deviceInfoContent: {
- flex: 1,
- },
- hintBox: {
- backgroundColor: theme.colors.primary + '10',
- borderRadius: 8,
- padding: 6,
- borderLeftWidth: 3,
- borderLeftColor: theme.colors.primary,
- },
- hintText: {
- fontSize: 13,
- color: theme.colors.text,
- lineHeight: 18,
- },
- sendModeDeviceItem: {
- flexDirection: 'row',
- alignItems: 'center',
- backgroundColor: theme.colors.cardBackground,
- borderRadius: 10,
- paddingVertical: 10,
- paddingHorizontal: 12,
- marginBottom: 8,
- borderWidth: 1,
- borderColor: theme.colors.border + '30',
- },
- sendModeDeviceItemSelected: {
- borderColor: theme.colors.primary,
- borderWidth: 1.5,
- backgroundColor: theme.colors.primary + '08',
- },
- sendModeDeviceIcon: {
- width: 20,
- height: 20,
- tintColor: theme.colors.primary,
- marginRight: 10,
- },
- sendModeDeviceContent: {
- flex: 1,
- flexDirection: 'row',
- alignItems: 'center',
- justifyContent: 'space-between',
- },
- sendModeDeviceLabel: {
- fontSize: 14,
- fontWeight: '600',
- color: theme.colors.text,
- fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace',
- },
- sendModeDeviceNpub: {
- fontSize: 12,
- color: theme.colors.textSecondary,
- fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace',
- marginLeft: 8,
- },
- sendModeDeviceBadge: {
- fontSize: 11,
- color: theme.colors.primary,
- marginTop: 2,
- fontWeight: '500',
- },
- sendModeCheckbox: {
- width: 22,
- height: 22,
- borderRadius: 6,
- borderWidth: 2,
- borderColor: theme.colors.border,
- backgroundColor: theme.colors.cardBackground,
- alignItems: 'center',
- justifyContent: 'center',
- marginLeft: 8,
- },
- sendModeCheckboxChecked: {
- borderColor: theme.colors.primary,
- backgroundColor: theme.colors.primary,
- },
- sendModeCheckmark: {
- color: theme.colors.background,
- fontSize: 14,
- fontWeight: '700',
- },
- buttonHalf: {
- flex: 0.48,
- },
- buttonCompact: {
- flex: 1,
- backgroundColor: 'transparent',
- borderRadius: 8,
- paddingVertical: 10,
- paddingHorizontal: 12,
- alignItems: 'center',
- justifyContent: 'center',
- flexDirection: 'row',
- gap: 6,
- borderWidth: 1.5,
- borderColor: theme.colors.border,
- },
- buttonTextCompact: {
- fontSize: 14,
- fontWeight: '600',
- },
- iconImageCompact: {
- width: 18,
- height: 18,
- tintColor: theme.colors.primary,
- },
- iconImage: {
- width: 24,
- height: 24,
- tintColor: theme.colors.primary,
- },
- iconPrepare: {
- width: 24,
- height: 24,
- tintColor: theme.colors.textOnPrimary,
- },
- iconShare: {
- width: 24,
- height: 24,
- tintColor: theme.colors.textOnPrimary,
- },
- checkIconLeft: {
- width: 20,
- height: 20,
- tintColor: '#4CAF50',
- marginRight: 8,
- },
- qrContainer: {
- backgroundColor: 'white',
- padding: 16,
- borderRadius: 12,
- alignItems: 'center',
- marginBottom: 16,
- },
- connectionDetailsText: {
- fontSize: 12,
- fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace',
- color: theme.colors.textSecondary,
- marginBottom: 12,
- textAlign: 'center',
- },
- buttonRow: {
- flexDirection: 'row',
- gap: 12,
- marginBottom: 12,
- },
- button: {
- flex: 1,
- backgroundColor: theme.colors.primary,
- borderRadius: 12,
- paddingVertical: 14,
- alignItems: 'center',
- justifyContent: 'center',
- flexDirection: 'row',
- gap: 8,
- },
- buttonSecondary: {
- backgroundColor: 'transparent',
- borderWidth: 2,
- borderColor: theme.colors.border,
- },
- buttonText: {
- color: theme.colors.background,
- fontSize: 16,
- fontWeight: '600',
- },
- buttonTextSecondary: {
- color: theme.colors.secondary,
- },
- buttonDisabled: {
- opacity: 0.5,
- },
- statusIndicator: {
- width: 8,
- height: 8,
- borderRadius: 4,
- backgroundColor: theme.colors.primary,
- marginRight: 8,
- },
- modalSubtitle: {
- fontSize: 14,
- color: theme.colors.textSecondary,
- marginBottom: 16,
- fontFamily: Platform.OS === 'ios' ? 'System' : 'Roboto',
- textAlign: 'center',
- lineHeight: 20,
- },
- progressCircle: {
- marginBottom: 16,
- },
- progressTextWrapper: {
- position: 'absolute',
- top: 0,
- left: 0,
- right: 0,
- bottom: 0,
- justifyContent: 'center',
- alignItems: 'center',
- },
- progressPercentage: {
- fontSize: 14,
- fontWeight: 'bold',
- color: theme.colors.text,
- fontFamily: Platform.OS === 'ios' ? 'System' : 'Roboto',
- textAlign: 'center',
- marginBottom: 16,
- },
- modalIconContainer: {
- marginBottom: 10,
- alignItems: 'center',
- },
- modalIconBackground: {
- width: 50,
- height: 50,
- borderRadius: 25,
- backgroundColor: theme.colors.primary + '20',
- alignItems: 'center',
- justifyContent: 'center',
- },
- finalizingModalIcon: {
- width: 24,
- height: 24,
- tintColor: theme.colors.primary,
- },
- statusContainer: {
- width: '100%',
- marginTop: 8,
- },
- statusRow: {
- flexDirection: 'row',
- alignItems: 'center',
- marginBottom: 8,
- },
- finalizingStatusText: {
- fontSize: 14,
- color: theme.colors.text,
- fontWeight: '500',
- fontFamily: Platform.OS === 'ios' ? 'System' : 'Roboto',
- flex: 1,
- },
- finalizingCountdownText: {
- fontSize: 13,
- color: theme.colors.textSecondary,
- fontFamily: Platform.OS === 'ios' ? 'System' : 'Roboto',
- textAlign: 'center',
- },
- statusCheck: {
- width: 20,
- height: 20,
- tintColor: theme.colors.primary,
- },
- statusText: {
- fontSize: 14,
- color: theme.colors.text,
- fontWeight: '500',
- },
- statusTextSecondary: {
- color: theme.colors.textSecondary,
- },
- progressContainer: {
- marginTop: 20,
- alignItems: 'center',
- },
- progressText: {
- fontSize: 14,
- color: theme.colors.textSecondary,
- marginTop: 8,
- },
- scannerContainer: {
- flex: 1,
- justifyContent: 'center',
- alignItems: 'center',
- },
- qrFrame: {
- width: 250,
- height: 250,
- borderWidth: 2,
- borderColor: theme.colors.primary,
- borderRadius: 12,
- },
- closeScannerButton: {
- position: 'absolute',
- bottom: 40,
- backgroundColor: theme.colors.primary,
- paddingHorizontal: 24,
- paddingVertical: 12,
- borderRadius: 12,
- },
- closeScannerButtonText: {
- color: theme.colors.background,
- fontSize: 16,
- fontWeight: '600',
- },
- cameraNotFound: {
- color: theme.colors.text,
- fontSize: 16,
- },
- sessionInfo: {
- marginTop: 12,
- padding: 12,
- backgroundColor: 'rgba(0,0,0,0.02)',
- borderRadius: 8,
- },
- sessionInfoText: {
- fontSize: 11,
- fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace',
- color: theme.colors.textSecondary,
- marginBottom: 4,
- },
- modalOverlay: {
- flex: 1,
- backgroundColor: 'rgba(0,0,0,0.75)',
- alignItems: 'center',
- justifyContent: 'center',
- },
- qrModalContent: {
- backgroundColor: theme.colors.cardBackground,
- borderRadius: 16,
- width: '85%',
- maxWidth: 400,
- shadowColor: '#000',
- shadowOffset: {width: 0, height: 10},
- shadowOpacity: 0.3,
- shadowRadius: 20,
- elevation: 10,
- overflow: 'hidden',
- },
- qrModalHeader: {
- flexDirection: 'row',
- justifyContent: 'space-between',
- alignItems: 'center',
- paddingHorizontal: 24,
- paddingTop: 24,
- paddingBottom: 16,
- borderBottomWidth: 1,
- borderBottomColor: theme.colors.border + '40',
- },
- qrModalTitle: {
- fontSize: 18,
- fontWeight: '700',
- color: theme.colors.text,
- },
- qrModalDescription: {
- fontSize: 14,
- color: theme.colors.textSecondary,
- textAlign: 'center',
- marginTop: 12,
- lineHeight: 20,
- },
- qrModalCloseButton: {
- width: 32,
- height: 32,
- borderRadius: 16,
- backgroundColor: theme.colors.subPrimary + '10',
- alignItems: 'center',
- justifyContent: 'center',
- borderWidth: 1,
- borderColor: theme.colors.border + '10',
- },
- qrModalCloseText: {
- fontSize: 18,
- color: theme.colors.text,
- fontWeight: '600',
- },
- qrModalBody: {
- padding: 24,
- alignItems: 'center',
- },
- headerRow: {
- flexDirection: 'row',
- justifyContent: 'space-between',
- alignItems: 'center',
- },
- headerContent: {
- flex: 1,
- alignItems: 'center',
- },
- sectionSubtitle: {
- fontSize: 14,
- color: theme.colors.textSecondary,
- marginTop: 4,
- },
- helpButton: {
- width: 36,
- height: 36,
- borderRadius: 18,
- backgroundColor: theme.colors.primary + '20',
- alignItems: 'center',
- justifyContent: 'center',
- marginRight: 12,
- },
- helpIcon: {
- width: 20,
- height: 20,
- tintColor: theme.colors.primary,
- },
- stepIndicatorContainer: {
- marginBottom: 8,
- paddingVertical: 8,
- },
- stepRow: {
- flexDirection: 'row',
- alignItems: 'center',
- justifyContent: 'center',
- marginBottom: 8,
- },
- stepCircle: {
- width: 32,
- height: 32,
- borderRadius: 16,
- backgroundColor: theme.colors.border + '40',
- alignItems: 'center',
- justifyContent: 'center',
- borderWidth: 2,
- borderColor: theme.colors.border,
- },
- stepCircleCompleted: {
- backgroundColor: theme.colors.primary,
- borderColor: theme.colors.primary,
- },
- stepNumber: {
- fontSize: 14,
- fontWeight: '700',
- color: theme.colors.textSecondary,
- },
- stepNumberCompleted: {
- color: theme.colors.background,
- },
- stepLine: {
- flex: 1,
- height: 2,
- backgroundColor: theme.colors.border + '40',
- marginHorizontal: 8,
- },
- stepLabels: {
- flexDirection: 'row',
- justifyContent: 'space-between',
- paddingHorizontal: 16,
- marginTop: 4,
- },
- stepLabel: {
- fontSize: 11,
- color: theme.colors.textSecondary,
- textAlign: 'center',
- flex: 1,
- },
- collapsibleHeader: {
- paddingVertical: 12,
- paddingHorizontal: 16,
- backgroundColor: theme.colors.cardBackground,
- borderRadius: 8,
- borderWidth: 1,
- borderColor: theme.colors.border + '40',
- },
- collapsibleHeaderText: {
- fontSize: 13,
- fontWeight: '600',
- color: theme.colors.textSecondary,
- },
- collapsibleContent: {
- marginTop: 8,
- padding: 16,
- backgroundColor: theme.colors.cardBackground,
- borderRadius: 8,
- borderWidth: 1,
- borderColor: theme.colors.border + '40',
- },
- sectionHeaderRow: {
- flexDirection: 'row',
- justifyContent: 'space-between',
- alignItems: 'center',
- marginBottom: 12,
- },
- primaryActionButton: {
- flexDirection: 'row',
- alignItems: 'center',
- backgroundColor: theme.colors.primary,
- paddingHorizontal: 16,
- paddingVertical: 10,
- borderRadius: 8,
- gap: 6,
- },
- emptyStateContainer: {
- alignItems: 'center',
- paddingVertical: 20,
- marginBottom: 16,
- },
- emptyStateIcon: {
- width: 48,
- height: 48,
- marginBottom: 12,
- opacity: 0.5,
- },
- emptyStateText: {
- fontSize: 13,
- color: theme.colors.textSecondary,
- textAlign: 'center',
- paddingHorizontal: 20,
- lineHeight: 18,
- },
- readyCard: {
- backgroundColor: theme.colors.primary + '10',
- borderColor: theme.colors.primary,
- borderWidth: 2,
- },
- helpModalBody: {
- maxHeight: 400,
- padding: 24,
- },
- helpSection: {
- marginBottom: 24,
- },
- helpTitle: {
- fontSize: 16,
- fontWeight: '700',
- color: theme.colors.text,
- marginBottom: 8,
- },
- helpText: {
- fontSize: 14,
- color: theme.colors.textSecondary,
- lineHeight: 20,
- },
- inputError: {
- borderColor: theme.colors.danger || '#FF3B30',
- backgroundColor: (theme.colors.danger || '#FF3B30') + '10',
- },
- inputSuccess: {
- borderColor: theme.colors.primary,
- backgroundColor: theme.colors.primary + '10',
- },
- inputValidating: {
- borderColor: theme.colors.textSecondary,
- backgroundColor: theme.colors.textSecondary + '05',
- },
- errorIndicator: {
- marginTop: 8,
- padding: 8,
- backgroundColor: (theme.colors.danger || '#FF3B30') + '10',
- borderRadius: 6,
- borderLeftWidth: 3,
- borderLeftColor: theme.colors.danger || '#FF3B30',
- },
- errorText: {
- fontSize: 12,
- color: theme.colors.danger || '#FF3B30',
- fontWeight: '500',
- },
- validatingIndicator: {
- width: 48,
- height: 48,
- alignItems: 'center',
- justifyContent: 'center',
- },
- validatingText: {
- fontSize: 18,
- color: theme.colors.textSecondary,
- fontWeight: '600',
- },
- checkboxContainer: {
- flexDirection: 'row',
- alignItems: 'center',
- marginTop: 16,
- paddingVertical: 8,
- },
- checkbox: {
- width: 24,
- height: 24,
- borderRadius: 6,
- borderWidth: 2,
- borderColor: theme.colors.border,
- backgroundColor: 'transparent',
- marginRight: 12,
- alignItems: 'center',
- justifyContent: 'center',
- },
- checkboxChecked: {
- backgroundColor: theme.colors.primary,
- borderColor: theme.colors.primary,
- },
- checkboxCheckmark: {
- color: theme.colors.background,
- fontSize: 16,
- fontWeight: '700',
- },
- checkboxLabel: {
- fontSize: 14,
- color: theme.colors.text,
- flex: 1,
- },
- preparingModalContent: {
- backgroundColor: theme.colors.cardBackground,
- borderRadius: 16,
- padding: 24,
- width: '90%',
- maxWidth: 400,
- alignItems: 'center',
- shadowColor: '#000',
- shadowOffset: {width: 0, height: 8},
- shadowOpacity: 0.25,
- shadowRadius: 16,
- elevation: 8,
- },
- preparingModalIconContainer: {
- marginBottom: 16,
- alignItems: 'center',
- },
- preparingModalIconBackground: {
- width: 64,
- height: 64,
- borderRadius: 32,
- backgroundColor: theme.colors.primary + '20',
- alignItems: 'center',
- justifyContent: 'center',
- },
- preparingModalIcon: {
- width: 32,
- height: 32,
- tintColor: theme.colors.primary,
- },
- preparingModalTitle: {
- fontSize: 20,
- fontWeight: '700',
- color: theme.colors.text,
- textAlign: 'center',
- marginBottom: 8,
- },
- preparingModalSubtitle: {
- fontSize: 14,
- color: theme.colors.textSecondary,
- textAlign: 'center',
- marginBottom: 24,
- },
- preparingProgressContainer: {
- width: '100%',
- alignItems: 'center',
- marginBottom: 16,
- },
- preparingProgressTrack: {
- width: 200,
- height: 6,
- backgroundColor: theme.colors.border + '40',
- borderRadius: 3,
- overflow: 'hidden',
- },
- preparingProgressBar: {
- height: '100%',
- borderRadius: 3,
- },
- preparingStatusContainer: {
- width: '100%',
- marginTop: 8,
- },
- preparingStatusRow: {
- flexDirection: 'row',
- alignItems: 'center',
- marginBottom: 8,
- justifyContent: 'center',
- },
- preparingStatusIndicator: {
- width: 8,
- height: 8,
- borderRadius: 4,
- backgroundColor: theme.colors.primary,
- marginRight: 8,
- },
- preparingStatusText: {
- fontSize: 14,
- color: theme.colors.text,
- fontWeight: '500',
- },
- preparingCountdownText: {
- fontSize: 13,
- color: theme.colors.textSecondary,
- textAlign: 'center',
- },
- informationCard: {
- backgroundColor: theme.colors.cardBackground,
- borderRadius: 16,
- padding: 20,
- marginBottom: 16,
- shadowColor: theme.colors.shadowColor,
- shadowOffset: {width: 0, height: 2},
- shadowOpacity: 0.1,
- shadowRadius: 8,
- elevation: 3,
- borderWidth: 1,
- borderColor: theme.colors.border,
- },
- backupButton: {
- marginTop: 12,
- backgroundColor: theme.colors.subPrimary,
- width: '100%',
- borderRadius: 12,
- paddingVertical: 14,
- paddingHorizontal: 16,
- alignItems: 'center',
- justifyContent: 'center',
- alignSelf: 'center',
- shadowColor: theme.colors.shadowColor,
- shadowOffset: {width: 0, height: 4},
- shadowOpacity: 0.15,
- shadowRadius: 8,
- elevation: 4,
- },
- backupButtonText: {
- color: theme.colors.background,
- fontSize: 16,
- fontWeight: '600',
- fontFamily: Platform.OS === 'ios' ? 'System' : 'Roboto',
- textAlign: 'center',
- lineHeight: 22,
- },
- backupConfirmationHeader: {
- flexDirection: 'row',
- alignItems: 'center',
- marginBottom: 12,
- },
- backupConfirmationIcon: {
- width: 24,
- height: 24,
- borderRadius: 12,
- backgroundColor: theme.colors.secondary,
- alignItems: 'center',
- justifyContent: 'center',
- marginRight: 12,
- },
- backupConfirmationIconText: {
- color: theme.colors.background,
- fontSize: 14,
- fontWeight: 'bold',
- },
- backupConfirmationTitle: {
- fontSize: 18,
- fontWeight: '700',
- color: theme.colors.text,
- fontFamily: Platform.OS === 'ios' ? 'System' : 'Roboto',
- },
- backupConfirmationDescription: {
- fontSize: 14,
- color: theme.colors.textSecondary,
- fontFamily: Platform.OS === 'ios' ? 'System' : 'Roboto',
- lineHeight: 20,
- marginBottom: 10,
- },
- backupConfirmationContainer: {
- marginBottom: 4,
- },
- enhancedBackupCheckbox: {
- flexDirection: 'row',
- alignItems: 'center',
- paddingVertical: 8,
- paddingHorizontal: 12,
- marginVertical: 3,
- borderRadius: 12,
- backgroundColor: 'transparent',
- },
- enhancedBackupCheckboxChecked: {
- backgroundColor: theme.colors.secondary + '15',
- },
- backupCheckboxContent: {
- flex: 1,
- marginLeft: 12,
- },
- backupCheckboxLabel: {
- fontSize: 15,
- fontWeight: '600',
- color: theme.colors.text,
- fontFamily: Platform.OS === 'ios' ? 'System' : 'Roboto',
- marginBottom: 2,
- },
- backupCheckboxHint: {
- fontSize: 12,
- color: theme.colors.textSecondary,
- fontFamily: Platform.OS === 'ios' ? 'System' : 'Roboto',
- fontStyle: 'italic',
- },
- backupCheckIcon: {
- width: 20,
- height: 20,
- // tintColor will be set conditionally in the component
- },
- enhancedCheckbox: {
- width: 24,
- height: 24,
- borderRadius: 6,
- borderWidth: 2,
- borderColor: theme.colors.primary,
- justifyContent: 'center',
- alignItems: 'center',
- backgroundColor: 'transparent',
- marginRight: 12,
- },
- enhancedCheckboxChecked: {
- backgroundColor: theme.colors.primary,
- borderColor: theme.colors.primary,
- },
- checkmark: {
- color: theme.colors.background,
- fontSize: 16,
- fontWeight: 'bold',
- },
- enhancedCheckboxContainer: {
- flexDirection: 'row',
- alignItems: 'center',
- paddingHorizontal: 8,
- marginVertical: 2,
- marginHorizontal: 4,
- paddingVertical: 2,
- borderRadius: 8,
- backgroundColor: 'transparent',
- },
- enhancedCheckboxContainerChecked: {
- backgroundColor: theme.colors.primary + '10',
- },
- checkboxTextContainer: {
- flex: 1,
- padding: 8,
- },
- enhancedCheckboxLabel: {
- fontSize: 15,
- fontWeight: '500',
- color: theme.colors.text,
- fontFamily: Platform.OS === 'ios' ? 'System' : 'Roboto',
- },
- warningHint: {
- fontSize: 12,
- color: theme.colors.textSecondary,
- fontFamily: Platform.OS === 'ios' ? 'System' : 'Roboto',
- marginTop: 2,
- fontStyle: 'italic',
- },
- warningIcon: {
- fontSize: 18,
- marginLeft: 8,
- },
- finalStepHeader: {
- flexDirection: 'row',
- alignItems: 'center',
- marginBottom: 12,
- padding: 12,
- backgroundColor: theme.colors.background,
- borderRadius: 12,
- },
- finalStepIconContainer: {
- marginRight: 12,
- },
- finalStepPhoneIcon: {
- width: 24,
- height: 24,
- tintColor: theme.colors.primary,
- },
- finalStepTextContainer: {
- flex: 1,
- },
- finalStepTitle: {
- fontSize: 18,
- fontWeight: '700',
- color: theme.colors.text,
- fontFamily: Platform.OS === 'ios' ? 'System' : 'Roboto',
- marginBottom: 4,
- },
- finalStepDescription: {
- fontSize: 14,
- color: theme.colors.textSecondary,
- marginBottom: 12,
- lineHeight: 20,
- fontFamily: Platform.OS === 'ios' ? 'System' : 'Roboto',
- },
- participantsList: {
- marginTop: 8,
- marginBottom: 12,
- paddingHorizontal: 12,
- paddingVertical: 12,
- backgroundColor: theme.colors.cardBackground + '80',
- borderRadius: 8,
- borderWidth: 1,
- borderColor: theme.colors.border + '30',
- },
- participantsListTitle: {
- fontSize: 13,
- fontWeight: '600',
- color: theme.colors.text,
- marginBottom: 8,
- },
- participantItem: {
- flexDirection: 'row',
- alignItems: 'flex-start',
- marginBottom: 8,
- },
- bulletPoint: {
- fontSize: 16,
- color: theme.colors.primary,
- marginRight: 8,
- marginTop: 2,
- fontWeight: 'bold',
- },
- participantText: {
- flex: 1,
- fontSize: 13,
- color: theme.colors.text,
- lineHeight: 18,
- },
- participantLabel: {
- fontWeight: '600',
- color: theme.colors.text,
- },
- localDeviceBadge: {
- fontSize: 12,
- fontWeight: '500',
- color: theme.colors.primary,
- fontStyle: 'italic',
- },
- participantNpub: {
- fontSize: 12,
- color: theme.colors.textSecondary,
- fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace',
- marginTop: 2,
- },
- participantDevicesInfo: {
- marginTop: 12,
- paddingTop: 12,
- borderTopWidth: 1,
- borderTopColor: theme.colors.border + '40',
- },
- participantDevicesInfoTitle: {
- fontSize: 14,
- fontWeight: '600',
- color: theme.colors.text,
- marginBottom: 10,
- },
- participantDeviceItem: {
- flexDirection: 'row',
- alignItems: 'center',
- justifyContent: 'space-between',
- marginBottom: 10,
- paddingVertical: 6,
- paddingHorizontal: 8,
- backgroundColor: theme.colors.cardBackground,
- borderRadius: 8,
- borderWidth: 1,
- borderColor: theme.colors.border + '30',
- },
- participantDeviceLeft: {
- flexDirection: 'row',
- alignItems: 'center',
- flex: 1,
- },
- participantDeviceIcon: {
- width: 18,
- height: 18,
- tintColor: theme.colors.primary,
- marginRight: 10,
- },
- participantDeviceLabel: {
- fontSize: 13,
- fontWeight: '600',
- color: theme.colors.text,
- },
- participantDeviceNpub: {
- fontSize: 12,
- color: theme.colors.textSecondary,
- fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace',
- textAlign: 'right',
- },
- twoPhonesContainer: {
- flexDirection: 'row',
- alignItems: 'center',
- marginLeft: 8,
- },
- threeDevicesContainer: {
- flexDirection: 'row',
- alignItems: 'center',
- justifyContent: 'space-between',
- },
- firstPhone: {
- marginLeft: 0,
- marginRight: -4,
- zIndex: 2,
- },
- secondPhone: {
- marginLeft: 0,
- opacity: 0.7,
- zIndex: 1,
- },
- thirdPhone: {
- marginLeft: 0,
- opacity: 0.5,
- zIndex: 0,
- },
- proceedButtonOn: {
- marginTop: 16,
- backgroundColor: theme.colors.primary,
- borderRadius: 12,
- paddingVertical: 16,
- paddingHorizontal: 24,
- alignItems: 'center',
- justifyContent: 'center',
- width: '100%',
- alignSelf: 'center',
- shadowColor: theme.colors.shadowColor,
- shadowOffset: {width: 0, height: 4},
- shadowOpacity: 0.15,
- shadowRadius: 8,
- elevation: 4,
- },
- proceedButtonOff: {
- marginTop: 16,
- backgroundColor: theme.colors.textSecondary,
- borderRadius: 12,
- paddingVertical: 16,
- paddingHorizontal: 24,
- alignItems: 'center',
- justifyContent: 'center',
- width: '100%',
- alignSelf: 'center',
- opacity: 0.6,
- },
- pairButtonText: {
- color: theme.colors.textOnPrimary,
- fontSize: 16,
- fontWeight: '600',
- fontFamily: Platform.OS === 'ios' ? 'System' : 'Roboto',
- },
- buttonContent: {
- flexDirection: 'row',
- alignItems: 'center',
- justifyContent: 'center',
- gap: 8,
- },
- buttonIcon: {
- width: 20,
- height: 20,
- tintColor: theme.colors.background,
- },
- modalContent: {
- backgroundColor: theme.colors.cardBackground,
- borderRadius: 20,
- padding: 24,
- width: '90%',
- maxWidth: 400,
- shadowColor: '#000',
- shadowOffset: {width: 0, height: 10},
- shadowOpacity: 0.3,
- shadowRadius: 20,
- elevation: 10,
- },
- modalHeader: {
- flexDirection: 'row',
- alignItems: 'center',
- marginBottom: 16,
- },
- modalIcon: {
- width: 32,
- height: 32,
- marginRight: 12,
- tintColor: theme.colors.primary,
- },
- modalTitle: {
- fontSize: 20,
- fontWeight: '700',
- color: theme.colors.text,
- fontFamily: Platform.OS === 'ios' ? 'System' : 'Roboto',
- },
- modalDescription: {
- fontSize: 14,
- color: theme.colors.textSecondary,
- marginBottom: 20,
- fontFamily: Platform.OS === 'ios' ? 'System' : 'Roboto',
- lineHeight: 20,
- },
- passwordContainer: {
- marginBottom: 16,
- },
- passwordLabel: {
- fontSize: 14,
- fontWeight: '600',
- color: theme.colors.text,
- marginBottom: 8,
- fontFamily: Platform.OS === 'ios' ? 'System' : 'Roboto',
- },
- passwordInputContainer: {
- flexDirection: 'row',
- alignItems: 'center',
- backgroundColor: theme.colors.background,
- borderRadius: 12,
- borderWidth: 1,
- borderColor: theme.colors.border,
- paddingHorizontal: 12,
- },
- passwordInput: {
- flex: 1,
- paddingVertical: 12,
- fontSize: 16,
- color: theme.colors.text,
- fontFamily: Platform.OS === 'ios' ? 'System' : 'Roboto',
- },
- eyeButton: {
- padding: 8,
- },
- eyeIcon: {
- width: 20,
- height: 20,
- tintColor: theme.colors.textSecondary,
- },
- strengthContainer: {
- marginTop: 8,
- },
- strengthBar: {
- height: 4,
- backgroundColor: theme.colors.border,
- borderRadius: 2,
- overflow: 'hidden',
- marginBottom: 4,
- },
- strengthFill: {
- height: '100%',
- borderRadius: 2,
- },
- strengthText: {
- fontSize: 12,
- fontWeight: '600',
- fontFamily: Platform.OS === 'ios' ? 'System' : 'Roboto',
- },
- requirementsContainer: {
- marginTop: 8,
- paddingLeft: 4,
- },
- requirementText: {
- fontSize: 12,
- color: '#FF6B35',
- marginBottom: 4,
- fontFamily: Platform.OS === 'ios' ? 'System' : 'Roboto',
- fontWeight: '500',
- },
- errorInput: {
- borderColor: theme.colors.danger || '#FF3B30',
- },
- modalActions: {
- flexDirection: 'row',
- justifyContent: 'space-between',
- marginTop: 8,
- gap: 12,
- },
- modalButton: {
- flex: 1,
- borderRadius: 12,
- paddingVertical: 14,
- alignItems: 'center',
- justifyContent: 'center',
- },
- cancelButton: {
- backgroundColor: theme.colors.border,
- },
- confirmButton: {
- backgroundColor: theme.colors.primary,
- },
- disabledButton: {
- opacity: 0.5,
- },
- cancelLinkContainer: {
- marginTop: 8,
- marginBottom: 4,
- alignItems: 'center',
- },
- cancelLinkText: {
- color: theme.colors.textSecondary,
- fontWeight: '600',
- textDecorationLine: 'underline',
- fontFamily: Platform.OS === 'ios' ? 'System' : 'Roboto',
- textAlign: 'center',
- fontSize: 14,
- marginTop: 12,
- },
- retryButton: {
- backgroundColor: theme.colors.secondary,
- borderRadius: 18,
- flexDirection: 'row',
- alignItems: 'center',
- justifyContent: 'center',
- paddingVertical: 8,
- paddingHorizontal: 12,
- shadowColor: theme.colors.shadowColor,
- shadowOffset: {width: 0, height: 1},
- shadowOpacity: 0.1,
- shadowRadius: 2,
- elevation: 2,
- minHeight: 36,
- },
- retryLink: {
- color: theme.colors.background,
- fontWeight: '600',
- fontFamily: Platform.OS === 'ios' ? 'System' : 'Roboto',
- textAlign: 'center',
- fontSize: 14,
- marginLeft: 6,
- },
- buttonFlex: {
- flex: 1,
- marginHorizontal: 6,
- },
- cancelSetupButton: {
- backgroundColor: theme.colors.background,
- borderColor: theme.colors.secondary,
- borderWidth: 1,
- borderRadius: 18,
- alignItems: 'center',
- justifyContent: 'center',
- paddingVertical: 8,
- paddingHorizontal: 12,
- minHeight: 36,
- },
- cancelLink: {
- color: theme.colors.secondary,
- fontWeight: '600',
- fontFamily: Platform.OS === 'ios' ? 'System' : 'Roboto',
- textAlign: 'center',
- fontSize: 14,
- },
- });
-
- return (
-
-
-
- {/* Hide all previous sections when mpcDone is true */}
- {!mpcDone &&
- (() => {
- // Check if Final Step should be shown
- const showFinalStep =
- !isSendBitcoin && !isSignPSBT &&
- isPreParamsReady &&
- localNpub &&
- deviceName &&
- ((isTrio &&
- peerNpub1 &&
- peerDeviceName1 &&
- peerNpub2 &&
- peerDeviceName2) ||
- (!isTrio && peerNpub1 && peerDeviceName1));
-
- return (
- <>
- {/* Header */}
-
-
- {/* Help button on the left */}
- {
- HapticFeedback.light();
- setShowHelpModal(true);
- }}
- activeOpacity={0.7}>
-
-
-
- {/* Title in the center */}
-
- {(isSendBitcoin || isSignPSBT) ? (
-
-
-
- {isSignPSBT ? 'PSBT Co-Signing' : 'Transaction Co-Signing'}
-
-
- ) : (
-
- Setup Wallet
-
- )}
-
-
- {/* Abort Setup button on the right */}
- {!mpcDone && !isPairing ? (
- {
- HapticFeedback.light();
- if (isSendBitcoin || isSignPSBT) {
- navigation.goBack();
- } else {
- navigation.dispatch(
- CommonActions.reset({
- index: 0,
- routes: [{name: 'Welcome'}],
- }),
- );
- }
- }}
- activeOpacity={0.7}>
-
- {(isSendBitcoin || isSignPSBT) ? 'Cancel' : 'Abort'}
-
-
- ) : (
-
- )}
-
-
-
- {/* PSBT Info Section */}
- {isSignPSBT && route.params?.psbtBase64 && (
-
-
-
- PSBT Ready to Sign
-
- {psbtDetails ? (
- <>
-
-
- Inputs:
-
-
- {psbtDetails.inputs.length}
-
-
-
-
- Outputs:
-
-
- {psbtDetails.outputs.length}
-
-
-
-
- Total Input:
-
-
- {sat2btcStr(psbtDetails.totalInput)} BTC
-
-
-
-
- Total Output:
-
-
- {sat2btcStr(psbtDetails.totalOutput)} BTC
-
-
-
-
- Fee:
-
-
- {sat2btcStr(psbtDetails.fee)} BTC
-
-
- {psbtDetails.derivePath && (
-
-
- Derivation Path:
-
-
- {psbtDetails.derivePath}
-
-
- )}
- {psbtDetails.derivePaths &&
- psbtDetails.derivePaths.length > 1 && (
-
-
- {psbtDetails.derivePaths.length} different paths
-
-
- )}
- >
- ) : (
-
- {route.params.psbtBase64
- ? `PSBT (${Math.round(
- (route.params.psbtBase64.length || 0) / 1024,
- )} KB) - Parsing...`
- : 'No PSBT data'}
-
- )}
-
-
- )}
-
- {/* Send Mode: Device Selection - Show current device and allow selecting one other */}
- {(isSendBitcoin || isSignPSBT) && (
-
-
- This Device
-
- {sendModeDevices.length === 0 ? (
-
- Loading...
-
- ) : (
- (() => {
- // Separate local and other devices
- const localDevice = sendModeDevices.find(
- d => d.isLocal,
- );
- const otherDevices = sendModeDevices
- .filter(d => !d.isLocal)
- .sort((a, b) => a.npub.localeCompare(b.npub));
-
- return (
- <>
- {/* Current Device */}
- {localDevice && (
-
-
-
-
-
- {localDevice.keyshareLabel}
-
-
- This device
-
-
-
- {shortenNpub(localDevice.npub, 8, 6)}
-
-
-
- )}
-
- {/* Select One Other Device */}
- {otherDevices.length > 0 && (
- <>
-
- {isTrio
- ? 'Select one device to co-sign:'
- : 'Co-signing device:'}
-
- {otherDevices.map(dev => {
- // In duo mode, use View (not selectable)
- // In trio mode, use TouchableOpacity (selectable)
- if (!isTrio) {
- return (
-
-
-
-
- {dev.keyshareLabel}
-
-
- {shortenNpub(dev.npub, 8, 6)}
-
-
- {selectedPeerNpub === dev.npub && (
-
-
- ✓
-
-
- )}
-
- );
- }
-
- // Trio mode: selectable
- return (
- {
- HapticFeedback.medium();
- // In trio, allow user to select any device
- // If clicking the same device, deselect (allow empty selection)
- // If clicking different device, select that one
- setSelectedPeerNpub(
- selectedPeerNpub === dev.npub
- ? ''
- : dev.npub,
- );
- dbg(
- 'User selected peer in trio mode:',
- dev.npub === selectedPeerNpub
- ? 'deselected'
- : dev.npub.substring(0, 20) +
- '...',
- );
- }}
- activeOpacity={0.7}>
-
-
-
- {dev.keyshareLabel}
-
-
- {shortenNpub(dev.npub, 8, 6)}
-
-
-
- {selectedPeerNpub === dev.npub && (
-
- ✓
-
- )}
-
-
- );
- })}
- >
- )}
- >
- );
- })()
- )}
-
- )}
-
- {/* Relay Configuration - Collapsible - Hide when Final Step is shown */}
- {!showFinalStep && (
-
- {
- HapticFeedback.light();
- setShowRelayConfig(!showRelayConfig);
- }}
- activeOpacity={0.7}>
-
- {showRelayConfig ? '▼' : '▶'} Advanced: Nostr Relays
- Settings
-
-
- {showRelayConfig && (
-
-
- Configure Nostr relays (defaults work for most
- users). Enter relay URLs, one per line or
- comma-separated (wss://...).
-
-
-
- )}
-
- )}
-
- {/* Step Indicator */}
- {!isSendBitcoin && !isSignPSBT && (
-
-
-
-
- {localNpub ? '✓' : '1'}
-
-
-
-
-
- {peerNpub1 && peerDeviceName1 ? '✓' : '2'}
-
-
- {isTrio && (
- <>
-
-
-
- {peerNpub2 && peerDeviceName2 ? '✓' : '3'}
-
-
- >
- )}
-
-
-
- {isPreParamsReady ? '✓' : isTrio ? '4' : '3'}
-
-
-
-
-
- {canStartKeygen ? '✓' : isTrio ? '5' : '4'}
-
-
-
-
- Your Device
- 2nd Peer
- {isTrio && (
- 3rd Peer
- )}
- Prepared
- Ready
-
-
- )}
-
- {/* Local Device Card - Hide when Final Step is shown or in send mode */}
- {localNpub &&
- deviceName &&
- partialNonce &&
- !isSendBitcoin && !isSignPSBT && (
-
-
- This Device
-
-
-
- {deviceName}@{shortenNpub(localNpub, 8, 6)}
-
-
-
-
-
-
-
-
-
-
-
- )}
-
- {/* Peer Connection 1 - Hide when Final Step is shown or in send/sign mode */}
- {!showFinalStep && !isSendBitcoin && !isSignPSBT && (
-
-
- {isTrio
- ? 'Step 2: Second Device'
- : 'Step 2: Other Device'}
-
-
-
- {peerNpub1 && (
-
- )}
- {peerNpub1 && peerDeviceName1 ? (
-
- {formatConnectionDisplay(
- peerNpub1,
- peerDeviceName1,
- )}
-
- ) : (
- {
- setPeerConnectionDetails1(text);
- handlePeerConnectionInput(text, 1);
- }}
- placeholder="Paste or scan connection details"
- placeholderTextColor={
- theme.colors.textSecondary + '80'
- }
- autoCapitalize="none"
- autoCorrect={false}
- />
- )}
- {peerInputValidating1 && (
-
- ...
-
- )}
- {peerNpub1 && !peerInputValidating1 && (
- clearPeerConnection(1)}
- activeOpacity={0.7}>
-
-
- )}
- {!peerNpub1 && !peerInputValidating1 && (
- <>
- handlePaste(1)}
- activeOpacity={0.7}>
-
-
- {
- HapticFeedback.light();
- const peerNum: 1 | 2 = 1;
- scanningForPeerRef.current = peerNum; // Update ref immediately
- // FOSS version: Use BarcodeZxingScan for both iOS and Android
- BarcodeZxingScan.showQrReader(
- (error: any, data: any) => {
- if (!error && data) {
- handleQRScan(data, peerNum);
- }
- },
- );
- }}
- activeOpacity={0.7}>
-
-
- >
- )}
-
- {peerInputError1 && (
-
-
- ⚠ {peerInputError1}
-
-
- )}
-
-
- )}
-
- {/* Peer Connection 2 (Trio only) - Hide when Final Step is shown or in send/sign mode */}
- {isTrio && !showFinalStep && !isSendBitcoin && !isSignPSBT && (
-
-
- Step 3: Third Device
-
-
-
- {peerNpub2 && (
-
- )}
- {peerNpub2 && peerDeviceName2 ? (
-
- {formatConnectionDisplay(
- peerNpub2,
- peerDeviceName2,
- )}
-
- ) : (
- {
- setPeerConnectionDetails2(text);
- handlePeerConnectionInput(text, 2);
- }}
- placeholder="Paste or scan connection details"
- placeholderTextColor={
- theme.colors.textSecondary + '80'
- }
- autoCapitalize="none"
- autoCorrect={false}
- />
- )}
- {peerInputValidating2 && (
-
- ...
-
- )}
- {peerNpub2 && !peerInputValidating2 && (
- clearPeerConnection(2)}
- activeOpacity={0.7}>
-
-
- )}
- {!peerNpub2 && !peerInputValidating2 && (
- <>
- handlePaste(2)}
- activeOpacity={0.7}>
-
-
- {
- HapticFeedback.light();
- const peerNum: 1 | 2 = 2;
- scanningForPeerRef.current = peerNum; // Update ref immediately
- // FOSS version: Use BarcodeZxingScan for both iOS and Android
- BarcodeZxingScan.showQrReader(
- (error: any, data: any) => {
- if (!error && data) {
- handleQRScan(data, peerNum);
- }
- },
- );
- }}
- activeOpacity={0.7}>
-
-
- >
- )}
-
- {peerInputError2 && (
-
-
- ⚠ {peerInputError2}
-
-
- )}
-
-
- )}
-
- {/* Prepare Device Section - Hide in send/sign mode */}
- {!isSendBitcoin && !isSignPSBT &&
- !isPreParamsReady &&
- localNpub &&
- deviceName &&
- ((isTrio &&
- peerNpub1 &&
- peerDeviceName1 &&
- peerNpub2 &&
- peerDeviceName2) ||
- (!isTrio && peerNpub1 && peerDeviceName1)) && (
-
-
-
-
-
- {isPreparing ? 'Preparing...' : 'Prepare Device'}
-
-
- {
- HapticFeedback.light();
- setIsPrepared(!isPrepared);
- }}>
-
- {isPrepared && (
- ✓
- )}
-
-
- Keep app open during setup
-
-
-
-
- )}
-
- {/* Preparing Modal */}
- {isPreparing && (
-
-
-
- {/* Icon Container */}
-
-
-
-
-
-
- {/* Header Text */}
-
- Preparing Device
-
-
- {/* Subtext */}
-
- Could take a while, given device specs. Do not leave
- the app during setup.
-
-
- {/* Loading Indicator */}
-
-
-
-
-
-
- {/* Status and Countdown */}
-
-
-
-
- Computing cryptographic params
-
-
-
- Time elapsed: {prepCounter} seconds
-
-
-
-
-
- )}
-
- {/* Help Modal */}
- setShowHelpModal(false)}>
-
-
-
- How It Works
- {
- HapticFeedback.medium();
- setShowHelpModal(false);
- }}
- activeOpacity={0.7}>
- ✕
-
-
-
-
-
-
-
- Step 1: This Device
-
-
-
- This device generates a unique ID. Share this with
- other devices by showing the QR code or copying
- the connection details.
-
-
-
-
-
-
- Step 2: Connect Peers
-
-
-
- On each peer device, scan your QR code or paste
- your connection details. Then share their
- connection details back to you.
-
-
-
-
-
-
- Step 3: Start
-
-
-
- Once all devices are prepared, tap proceed to Key
- Generation to begin the secure wallet setup
- process.
-
-
-
-
-
- Tips
-
-
- • QR scanning is the easiest method{'\n'}• Make
- sure all devices are online{'\n'}• The process
- takes 1-2 minutes
- {'\n'}• Keep devices close together
-
-
-
-
-
-
-
- {/* Final Step - Check other devices are prepared */}
- {!isSendBitcoin && !isSignPSBT &&
- isPreParamsReady &&
- !mpcDone &&
- localNpub &&
- deviceName &&
- ((isTrio &&
- peerNpub1 &&
- peerDeviceName1 &&
- peerNpub2 &&
- peerDeviceName2) ||
- (!isTrio && peerNpub1 && peerDeviceName1)) && (
-
-
-
-
-
-
-
- {isTrio && (
-
- )}
-
-
-
-
- Final Step
-
-
- Make sure{' '}
- {isTrio ? 'all devices' : 'both devices'}{' '}
- preparation step is complete.
-
-
-
-
- {/* Participants Device Information */}
- {Object.keys(keyshareMapping).length > 0 && (
-
-
- Participants:
-
- {keyshareMapping.keyshare1 && (
-
- •
-
-
- KeyShare1
-
- {keyshareMapping.keyshare1.isLocal && (
-
- {' '}
- (This device)
-
- )}
- {'\n'}
-
- {shortenNpub(
- keyshareMapping.keyshare1.npub,
- 8,
- 6,
- )}
-
-
-
- )}
- {keyshareMapping.keyshare2 && (
-
- •
-
-
- KeyShare2
-
- {keyshareMapping.keyshare2.isLocal && (
-
- {' '}
- (This device)
-
- )}
- {'\n'}
-
- {shortenNpub(
- keyshareMapping.keyshare2.npub,
- 8,
- 6,
- )}
-
-
-
- )}
- {keyshareMapping.keyshare3 && (
-
- •
-
-
- KeyShare3
-
- {keyshareMapping.keyshare3.isLocal && (
-
- {' '}
- (This device)
-
- )}
- {'\n'}
-
- {shortenNpub(
- keyshareMapping.keyshare3.npub,
- 8,
- 6,
- )}
-
-
-
- )}
-
- )}
-
- {
- HapticFeedback.medium();
- toggleKeygenReady();
- }}>
-
- {isKeygenReady && (
- ✓
- )}
-
-
-
- All devices are ready
-
-
-
-
- {/* Participant Devices Info */}
-
-
- Participants:
-
- {(() => {
- // Collect all participants
- const participants: Array<{
- npub: string;
- deviceName: string;
- }> = [];
-
- if (localNpub && deviceName) {
- participants.push({
- npub: localNpub,
- deviceName: deviceName,
- });
- }
- if (peerNpub1 && peerDeviceName1) {
- participants.push({
- npub: peerNpub1,
- deviceName: peerDeviceName1,
- });
- }
- if (isTrio && peerNpub2 && peerDeviceName2) {
- participants.push({
- npub: peerNpub2,
- deviceName: peerDeviceName2,
- });
- }
-
- // Sort by npub
- participants.sort((a, b) =>
- a.npub.localeCompare(b.npub),
- );
-
- return participants.map((participant, index) => (
-
-
-
-
- {participant.deviceName}
-
-
-
- {shortenNpub(participant.npub, 8, 6)}
-
-
- ));
- })()}
-
-
-
- )}
-
- {/* Transaction Summary - Show in send mode before button */}
- {isSendBitcoin && !isPairing && !mpcDone && route.params && (
-
-
-
-
- To Address
-
-
-
- {route.params?.toAddress || ''}
-
-
-
-
-
-
- Transaction Amount
-
-
-
- {sat2btcStr(route.params?.satoshiAmount)} BTC
-
-
- {route.params?.selectedCurrency || ''}{' '}
- {formatFiat(route.params?.fiatAmount)}
-
-
-
-
-
-
- Transaction Fee
-
-
-
- {sat2btcStr(route.params?.satoshiFees)} BTC
-
-
- {route.params?.selectedCurrency || ''}{' '}
- {formatFiat(route.params?.fiatFees)}
-
-
-
-
-
- )}
-
- {/* Readiness Checkbox for PSBT Signing */}
- {isSignPSBT && !isPairing && !mpcDone && (
-
-
-
- {isKeysignReady && (
- ✓
- )}
-
-
- Keep this app open during signing ⚠️
-
-
-
- )}
-
- {/* Start Button */}
- {!isPairing && !mpcDone && (
-
-
-
- {((isSendBitcoin || isSignPSBT) || !(isSendBitcoin || isSignPSBT)) && (
-
- )}
-
- {(isSendBitcoin || isSignPSBT)
- ? (() => {
- // Determine if local device is KeyShare1
- const localDevice = sendModeDevices.find(
- d => d.isLocal,
- );
- const isKeyShare1 =
- localDevice?.keyshareLabel === 'KeyShare1';
- return isKeyShare1
- ? (isSignPSBT ? 'Start PSBT Signing' : 'Start Co-Signing')
- : (isSignPSBT ? 'Join PSBT Signing' : 'Join Co-Signing');
- })()
- : (() => {
- // For keygen, determine if local npub is first in sorted order
- const allNpubs = [localNpub];
- if (peerNpub1) allNpubs.push(peerNpub1);
- if (isTrio && peerNpub2)
- allNpubs.push(peerNpub2);
- const sortedNpubs = allNpubs.sort();
- const isKeyShare1 =
- sortedNpubs[0] === localNpub;
- return isKeyShare1
- ? 'Start Key Generation'
- : 'Join Key Generation';
- })()}
-
-
-
-
- )}
- >
- );
- })()}
-
- {/* Keygen Modal - Similar to MobilesPairing */}
- {isPairing && !isSendBitcoin && !isSignPSBT && (
-
-
-
- {/* Icon Container */}
-
-
-
-
-
-
- {/* Header Text */}
- Finalizing Your Wallet
-
- {/* Subtext */}
-
- Securing your wallet with advanced cryptography. Please stay
- in the app...
-
-
- {/* Progress Container */}
-
- {/* Circular Progress */}
-
-
- {/* Progress Percentage */}
-
-
- {Math.round(progress)}%
-
-
-
-
- {/* Status and Countdown */}
-
-
-
-
- {status || 'Processing cryptographic operations'}
-
-
-
- Time elapsed: {prepCounter} seconds
-
-
-
-
-
- )}
-
- {/* Co-Signing Modal - Similar to MobilesPairing send_btc and sign_psbt */}
- {isPairing && (isSendBitcoin || isSignPSBT) && (
-
-
-
- {/* Icon Container */}
-
-
-
-
-
-
- {/* Header Text */}
-
- {isSignPSBT ? 'PSBT Co-Signing' : 'Co-Signing Your Transaction'}
-
-
- {/* Subtext */}
-
- Securing your transaction with multi-party cryptography.
- Please stay in the app...
-
-
- {/* Progress Container */}
-
- {/* Circular Progress */}
-
-
- {/* Progress Percentage */}
-
-
- {Math.round(progress)}%
-
-
-
-
- {/* Status and Countdown */}
-
-
-
-
- {status || 'Processing multi-party signature'}
-
-
-
- Time elapsed: {prepCounter} seconds
-
-
-
-
-
- )}
-
- {/* Success and Backup UI - Only show for keygen, not for send BTC or sign PSBT */}
- {mpcDone && !isSendBitcoin && !isSignPSBT && (
- <>
- {/* Keyshare Created Success */}
-
-
-
-
-
- Keyshare Created!
-
-
-
- Create secure backups of your keyshares. Store each device's
- backup in different locations to prevent single points of
- failure.
-
-
- {
- HapticFeedback.medium();
- setIsBackupModalVisible(true);
- }}>
-
-
-
- Backup{' '}
- {keyshareMapping.keyshare1?.isLocal
- ? 'KeyShare1'
- : keyshareMapping.keyshare2?.isLocal
- ? 'KeyShare2'
- : keyshareMapping.keyshare3?.isLocal
- ? 'KeyShare3'
- : 'Keyshare'}
-
-
-
-
-
-
- {/* Backup Confirmation */}
-
-
-
-
- ✓
-
-
- Confirm Backups
-
-
-
- Verify that {isTrio ? 'all devices' : 'both devices'} have
- successfully backed up their keyshares.
-
-
-
- {(() => {
- // Build device list based on keyshare mapping (sorted order)
- const devices = [];
- if (keyshareMapping.keyshare1) {
- devices.push({
- key: 'deviceOne',
- label: `KeyShare1 (${keyshareMapping.keyshare1.deviceName}) backed up`,
- device: keyshareMapping.keyshare1.deviceName,
- keyshareLabel: 'KeyShare1',
- });
- }
- if (keyshareMapping.keyshare2) {
- devices.push({
- key: 'deviceTwo',
- label: `KeyShare2 (${keyshareMapping.keyshare2.deviceName}) backed up`,
- device: keyshareMapping.keyshare2.deviceName,
- keyshareLabel: 'KeyShare2',
- });
- }
- if (keyshareMapping.keyshare3) {
- devices.push({
- key: 'deviceThree',
- label: `KeyShare3 (${keyshareMapping.keyshare3.deviceName}) backed up`,
- device: keyshareMapping.keyshare3.deviceName,
- keyshareLabel: 'KeyShare3',
- });
- }
- return devices;
- })().map(item => (
- {
- HapticFeedback.medium();
- toggleBackedup(item.key as keyof typeof backupChecks);
- }}>
-
- {backupChecks[
- item.key as keyof typeof backupChecks
- ] && ✓}
-
-
-
- {item.label}
-
-
- {item.keyshareLabel} ({item.device}) secured
-
-
-
-
- ))}
-
-
- {
- HapticFeedback.medium();
- navigation.dispatch(
- CommonActions.reset({
- index: 0,
- routes: [{name: 'Home'}],
- }),
- );
- }}
- disabled={!allBackupChecked}>
-
-
- Continue
-
-
-
-
- >
- )}
-
-
-
- {/* QR Code Modal */}
- setIsQRModalVisible(false)}>
-
-
-
- Connection Details
- {
- HapticFeedback.medium();
- setIsQRModalVisible(false);
- }}
- activeOpacity={0.7}>
- ✕
-
-
-
-
- {
- connectionQrRef.current = ref;
- }}
- />
-
-
- {shortenNpub(connectionDetails)}
-
-
-
-
- Share
-
-
-
-
-
-
-
- {/* Backup Modal */}
-
-
- {
- HapticFeedback.light();
- Keyboard.dismiss();
- }}>
- {
- HapticFeedback.light();
- }}>
-
-
- Backup Keyshare
-
-
- Create an encrypted backup of your keyshare, protected by a
- strong password.
-
-
-
- Set a Password
-
-
- {
- HapticFeedback.medium();
- setPasswordVisible(!passwordVisible);
- }}>
-
-
-
-
- {/* Password Strength Indicator */}
- {password.length > 0 && (
-
-
-
-
-
- {getPasswordStrengthText()}
-
-
- )}
-
- {/* Password Requirements */}
- {passwordErrors.length > 0 && (
-
- {passwordErrors.map((error, index) => (
-
- • {error}
-
- ))}
-
- )}
-
-
-
- Confirm Password
-
- 0 &&
- password !== confirmPassword &&
- styles.errorInput,
- ]}
- placeholder="Confirm your password"
- secureTextEntry={!confirmPasswordVisible}
- value={confirmPassword}
- onChangeText={setConfirmPassword}
- autoCapitalize="none"
- autoCorrect={false}
- />
- {
- HapticFeedback.medium();
- setConfirmPasswordVisible(!confirmPasswordVisible);
- }}>
-
-
-
- {confirmPassword.length > 0 && password !== confirmPassword && (
- Passwords do not match
- )}
-
-
-
- {
- HapticFeedback.medium();
- clearBackupModal();
- }}>
- Cancel
-
- {
- HapticFeedback.medium();
- backupShare();
- }}
- disabled={
- !password ||
- !confirmPassword ||
- password !== confirmPassword ||
- passwordStrength < 3
- }>
-
-
- Backup
-
-
-
-
-
-
-
-
- );
-};
-
-export default MobileNostrPairing;
diff --git a/screens/MobileNostrPairing.tsx b/screens/MobileNostrPairing.tsx
index b2865fc..aec391e 100644
--- a/screens/MobileNostrPairing.tsx
+++ b/screens/MobileNostrPairing.tsx
@@ -23,13 +23,20 @@ import DeviceInfo from 'react-native-device-info';
import EncryptedStorage from 'react-native-encrypted-storage';
import QRCode from 'react-native-qrcode-svg';
import Clipboard from '@react-native-clipboard/clipboard';
-import BarcodeZxingScan from 'rn-barcode-zxing-scan';
-import {Camera, useCameraDevice} from 'react-native-vision-camera';
+import QRScanner from '../components/QRScanner';
import * as Progress from 'react-native-progress';
import {CommonActions, RouteProp, useRoute} from '@react-navigation/native';
import {SafeAreaView} from 'react-native-safe-area-context';
import Big from 'big.js';
-import {dbg, HapticFeedback, getNostrRelays, getKeyshareLabel, hexToString, getDerivePathForNetwork} from '../utils';
+import {
+ dbg,
+ HapticFeedback,
+ getNostrRelays,
+ getKeyshareLabel,
+ hexToString,
+ getDerivePathForNetwork,
+ isLegacyWallet,
+} from '../utils';
import {useTheme} from '../theme';
import {useUser} from '../context/UserContext';
import LocalCache from '../services/LocalCache';
@@ -38,92 +45,6 @@ import RNFS from 'react-native-fs';
const {BBMTLibNativeModule} = NativeModules;
-// QR Scanner Component (moved outside to avoid re-render issues)
-const QRScannerComponent = ({
- cameraDevice,
- onScan,
- onClose,
- theme,
-}: {
- cameraDevice: any;
- onScan: (data: string) => void;
- onClose: () => void;
- theme: any;
-}) => {
- const scannerStyles = StyleSheet.create({
- scannerContainer: {
- flex: 1,
- justifyContent: 'center',
- alignItems: 'center',
- },
- qrFrame: {
- width: 250,
- height: 250,
- borderWidth: 2,
- borderColor: theme.colors.primary,
- borderRadius: 12,
- },
- closeScannerButton: {
- position: 'absolute',
- bottom: 40,
- backgroundColor: theme.colors.primary,
- paddingHorizontal: 24,
- paddingVertical: 12,
- borderRadius: 12,
- },
- closeScannerButtonText: {
- color: theme.colors.background,
- fontSize: 16,
- fontWeight: '600',
- },
- cameraNotFound: {
- color: theme.colors.text,
- fontSize: 16,
- },
- });
-
- if (!cameraDevice) {
- return (
-
- Camera Not Found
-
- Close
-
-
- );
- }
-
- const codeScanner = {
- codeTypes: ['qr' as const],
- onCodeScanned: (codes: any[]) => {
- if (codes.length > 0) {
- onScan(codes[0].value);
- }
- },
- };
-
- return (
-
-
-
-
- Close
-
-
- );
-};
-
type RouteParams = {
mode?: string; // 'duo' | 'trio' | 'send_btc' | 'sign_psbt'
addressType?: string;
@@ -135,7 +56,6 @@ type RouteParams = {
selectedCurrency?: string;
spendingHash?: string;
psbtBase64?: string; // For PSBT signing mode
- derivePath?: string; // BIP32 derivation path for PSBT
};
const MobileNostrPairing = ({navigation}: any) => {
@@ -200,7 +120,6 @@ const MobileNostrPairing = ({navigation}: any) => {
fee: number;
totalInput: number;
totalOutput: number;
- derivePath?: string;
derivePaths?: string[];
} | null>(null);
const [isPreParamsReady, setIsPreParamsReady] = useState(false);
@@ -252,13 +171,6 @@ const MobileNostrPairing = ({navigation}: any) => {
const [showHelpModal, setShowHelpModal] = useState(false);
const connectionQrRef = useRef(null);
- // Only use camera hooks on iOS - Android uses BarcodeZxingScan
- let device: any = null;
- if (Platform.OS === 'ios') {
- // eslint-disable-next-line react-hooks/rules-of-hooks
- device = useCameraDevice('back');
- }
-
// Connection details for sharing (hex encoded)
const connectionDetails = React.useMemo(() => {
if (!localNpub || !deviceName || !partialNonce) {
@@ -387,7 +299,10 @@ const MobileNostrPairing = ({navigation}: any) => {
route.params.psbtBase64,
);
- if (detailsJson.startsWith('error') || detailsJson.includes('failed')) {
+ if (
+ detailsJson.startsWith('error') ||
+ detailsJson.includes('failed')
+ ) {
dbg('Failed to parse PSBT details:', detailsJson);
setPsbtDetails(null);
return;
@@ -400,7 +315,6 @@ const MobileNostrPairing = ({navigation}: any) => {
fee: details.fee || 0,
totalInput: details.totalInput || 0,
totalOutput: details.totalOutput || 0,
- derivePath: details.derivePath,
derivePaths: details.derivePaths || [],
});
dbg('PSBT details parsed:', {
@@ -1737,7 +1651,24 @@ const MobileNostrPairing = ({navigation}: any) => {
const relaysCSV = relays.join(',');
const network = (await LocalCache.getItem('network')) || 'mainnet';
- const derivePath = getDerivePathForNetwork(network);
+ // Get address type from route params or cache, default to segwit-native
+ const currentAddressType =
+ addressType ||
+ (await LocalCache.getItem('addressType')) ||
+ 'segwit-native';
+ // Check if this is a legacy wallet (created before migration timestamp)
+ const useLegacyPath = isLegacyWallet(keyshare.created_at);
+ const derivePath = getDerivePathForNetwork(
+ network,
+ currentAddressType,
+ useLegacyPath,
+ );
+ dbg('Deriving path for Nostr send:', {
+ network,
+ currentAddressType,
+ useLegacyPath,
+ derivePath,
+ });
// Derive the public key from the root key using the derivation path
// This is critical - we need the DERIVED public key, not the root!
@@ -1852,7 +1783,10 @@ const MobileNostrPairing = ({navigation}: any) => {
}
if (!isKeysignReady) {
- Alert.alert('Not Ready', 'Please confirm that you are ready to sign the PSBT');
+ Alert.alert(
+ 'Not Ready',
+ 'Please confirm that you are ready to sign the PSBT',
+ );
return;
}
@@ -1929,19 +1863,33 @@ const MobileNostrPairing = ({navigation}: any) => {
// Find local npub
const localNpubFromKeyshare =
- allNpubsFromKeyshare.find(n => n === localNpub || (localNpub && n.startsWith(localNpub.substring(0, 20)))) ||
- localNpub;
+ allNpubsFromKeyshare.find(
+ n =>
+ n === localNpub ||
+ (localNpub && n.startsWith(localNpub.substring(0, 20))),
+ ) || localNpub;
// Build parties CSV
const allNpubs = [localNpubFromKeyshare];
if (isTrio) {
if (selectedPeerNpub) {
const selectedDevice = sendModeDevices.find(
- d => d.npub === selectedPeerNpub || (selectedPeerNpub.startsWith('npub1') && d.npub && d.npub.startsWith(selectedPeerNpub.substring(0, 20))),
+ d =>
+ d.npub === selectedPeerNpub ||
+ (selectedPeerNpub.startsWith('npub1') &&
+ d.npub &&
+ d.npub.startsWith(selectedPeerNpub.substring(0, 20))),
);
if (selectedDevice) {
- const selectedIndex = parseInt(selectedDevice.keyshareLabel.replace('KeyShare', ''), 10) - 1;
- if (selectedIndex >= 0 && selectedIndex < allNpubsFromKeyshare.length) {
+ const selectedIndex =
+ parseInt(
+ selectedDevice.keyshareLabel.replace('KeyShare', ''),
+ 10,
+ ) - 1;
+ if (
+ selectedIndex >= 0 &&
+ selectedIndex < allNpubsFromKeyshare.length
+ ) {
const fullPeerNpub = allNpubsFromKeyshare[selectedIndex];
if (fullPeerNpub !== localNpubFromKeyshare) {
allNpubs.push(fullPeerNpub);
@@ -1951,7 +1899,10 @@ const MobileNostrPairing = ({navigation}: any) => {
}
} else {
const fullPeerNpub = allNpubsFromKeyshare.find(
- n => n === selectedPeerNpub || (selectedPeerNpub.startsWith('npub1') && n.startsWith(selectedPeerNpub.substring(0, 20))),
+ n =>
+ n === selectedPeerNpub ||
+ (selectedPeerNpub.startsWith('npub1') &&
+ n.startsWith(selectedPeerNpub.substring(0, 20))),
);
if (fullPeerNpub && fullPeerNpub !== localNpubFromKeyshare) {
allNpubs.push(fullPeerNpub);
@@ -1963,7 +1914,9 @@ const MobileNostrPairing = ({navigation}: any) => {
throw new Error('Please select a peer device for trio mode');
}
} else {
- const otherNpubs = allNpubsFromKeyshare.filter(n => n !== localNpubFromKeyshare);
+ const otherNpubs = allNpubsFromKeyshare.filter(
+ n => n !== localNpubFromKeyshare,
+ );
if (otherNpubs.length > 0) {
allNpubs.push(otherNpubs[0]);
} else {
@@ -1973,24 +1926,10 @@ const MobileNostrPairing = ({navigation}: any) => {
const partiesNpubsCSV = allNpubs.sort().join(',');
const relaysCSV = relays.join(',');
- const network = (await LocalCache.getItem('network')) || 'mainnet';
- const derivePath = route.params?.derivePath || getDerivePathForNetwork(network);
-
- dbg('Using derivation path for PSBT signing:', derivePath);
-
- // Derive the public key
- const publicKey = await BBMTLibNativeModule.derivePubkey(
- keyshare.pub_key,
- keyshare.chain_code_hex,
- derivePath,
- );
-
dbg('Starting Nostr PSBT signing with:', {
relays: relaysCSV,
parties: partiesNpubsCSV.substring(0, 50) + '...',
npubsSorted: npubsSorted.substring(0, 30) + '...',
- derivePath,
- publicKey: publicKey.substring(0, 20) + '...',
psbtLength: route.params.psbtBase64?.length,
});
@@ -2005,7 +1944,11 @@ const MobileNostrPairing = ({navigation}: any) => {
);
// Validate result
- if (!signedPsbt || signedPsbt.includes('error') || signedPsbt.includes('failed')) {
+ if (
+ !signedPsbt ||
+ signedPsbt.includes('error') ||
+ signedPsbt.includes('failed')
+ ) {
throw new Error(signedPsbt || 'PSBT signing failed');
}
@@ -2064,7 +2007,7 @@ const MobileNostrPairing = ({navigation}: any) => {
maximumFractionDigits: 2,
}).format(Number(price));
- const sat2btcStr = (sats?: string) =>
+ const sat2btcStr = (sats?: string | number) =>
Big(sats || 0)
.div(1e8)
.toFixed(8);
@@ -2147,16 +2090,20 @@ const MobileNostrPairing = ({navigation}: any) => {
async function backupShare() {
if (!validatePassword(password)) {
dbg('❌ [BACKUP] Password validation failed');
+ const missingRequirements = passwordErrors.join('\n• ');
Alert.alert(
- 'Weak Password',
- 'Please use a stronger password that meets all requirements.',
+ 'Password Requirements Not Met',
+ `Your password must meet all of the following requirements:\n\n• ${missingRequirements}\n\nPlease update your password and try again.`,
);
return;
}
if (password !== confirmPassword) {
dbg('❌ [BACKUP] Password mismatch');
- Alert.alert('Password Mismatch', 'Passwords do not match.');
+ Alert.alert(
+ 'Passwords Do Not Match',
+ 'The password and confirmation password must be identical. Please check both fields and try again.',
+ );
return;
}
@@ -2171,17 +2118,34 @@ const MobileNostrPairing = ({navigation}: any) => {
await BBMTLibNativeModule.sha256(password),
);
- // Create friendly filename with date and time
- const now = new Date();
- const month = now.toLocaleDateString('en-US', {month: 'short'});
- const day = now.getDate().toString().padStart(2, '0');
- const year = now.getFullYear();
- const hours = now.getHours().toString().padStart(2, '0');
- const minutes = now.getMinutes().toString().padStart(2, '0');
- // Use keyshare label (KeyShare1/2/3) or fallback to local_party_key
+ // Create filename based on pub_key hash and keyshare number
+ if (!json.pub_key) {
+ Alert.alert('Error', 'Keyshare missing pub_key.');
+ return;
+ }
+
+ // Get SHA256 hash of pub_key and take first 4 characters
+ const pubKeyHash = await BBMTLibNativeModule.sha256(json.pub_key);
+ const hashPrefix = pubKeyHash.substring(0, 4).toLowerCase();
+
+ // Extract keyshare number from label (KeyShare1 -> 1, KeyShare2 -> 2, etc.)
const keyshareLabel = getKeyshareLabel(json);
- const shareName = keyshareLabel || json.local_party_key || 'keyshare';
- const friendlyFilename = `${shareName}.${month}${day}.${year}.${hours}${minutes}.share`;
+ let keyshareNumber = '1'; // default
+ if (keyshareLabel) {
+ const match = keyshareLabel.match(/KeyShare(\d+)/);
+ if (match) {
+ keyshareNumber = match[1];
+ }
+ } else if (json.keygen_committee_keys && json.local_party_key) {
+ // Fallback: compute from position in sorted keygen_committee_keys
+ const sortedKeys = [...json.keygen_committee_keys].sort();
+ const index = sortedKeys.indexOf(json.local_party_key);
+ if (index >= 0) {
+ keyshareNumber = String(index + 1);
+ }
+ }
+
+ const friendlyFilename = `${hashPrefix}K${keyshareNumber}.share`;
const tempDir = RNFS.TemporaryDirectoryPath || RNFS.CachesDirectoryPath;
const filePath = `${tempDir}/${friendlyFilename}`;
@@ -2190,6 +2154,7 @@ const MobileNostrPairing = ({navigation}: any) => {
await Share.open({
title: 'Backup Your Keyshare',
+ isNewTask: true,
message:
'Save this encrypted file securely. It is required for wallet recovery.',
url: `file://${filePath}`,
@@ -2198,9 +2163,12 @@ const MobileNostrPairing = ({navigation}: any) => {
failOnCancel: false,
});
+ // Cleanup temp file (best-effort)
try {
await RNFS.unlink(filePath);
- } catch {}
+ } catch {
+ // ignore cleanup errors
+ }
clearBackupModal();
} else {
Alert.alert('Error', 'Invalid keyshare.');
@@ -3792,7 +3760,8 @@ const MobileNostrPairing = ({navigation}: any) => {
(() => {
// Check if Final Step should be shown
const showFinalStep =
- !isSendBitcoin && !isSignPSBT &&
+ !isSendBitcoin &&
+ !isSignPSBT &&
isPreParamsReady &&
localNpub &&
deviceName &&
@@ -3825,7 +3794,7 @@ const MobileNostrPairing = ({navigation}: any) => {
{/* Title in the center */}
- {(isSendBitcoin || isSignPSBT) ? (
+ {isSendBitcoin || isSignPSBT ? (
{
fontFamily:
Platform.OS === 'ios' ? 'System' : 'Roboto',
}}>
- {isSignPSBT ? 'PSBT Co-Signing' : 'Transaction Co-Signing'}
+ {isSignPSBT
+ ? 'PSBT Co-Signing'
+ : 'Transaction Co-Signing'}
) : (
@@ -3863,7 +3834,7 @@ const MobileNostrPairing = ({navigation}: any) => {
{/* Abort Setup button on the right */}
- {!mpcDone && !isPairing ? (
+ {!mpcDone && !isPairing ? (
{
@@ -3881,7 +3852,7 @@ const MobileNostrPairing = ({navigation}: any) => {
}}
activeOpacity={0.7}>
- {(isSendBitcoin || isSignPSBT) ? 'Cancel' : 'Abort'}
+ {isSendBitcoin || isSignPSBT ? 'Cancel' : 'Abort'}
) : (
@@ -3998,14 +3969,15 @@ const MobileNostrPairing = ({navigation}: any) => {
color: theme.colors.text,
fontWeight: '600',
}}>
- {sat2btcStr(String(psbtDetails.totalOutput))} BTC
+ {sat2btcStr(String(psbtDetails.totalOutput))}{' '}
+ BTC
{
{sat2btcStr(String(psbtDetails.fee))} BTC
- {psbtDetails.derivePath && (
-
-
- Derivation Path:
-
-
- {psbtDetails.derivePath}
-
-
- )}
{psbtDetails.derivePaths &&
psbtDetails.derivePaths.length > 1 && (
{
fontSize: 10,
color: theme.colors.textSecondary,
}}>
- {psbtDetails.derivePaths.length} different paths
+ {psbtDetails.derivePaths.length} different
+ paths
)}
@@ -4442,7 +4388,8 @@ const MobileNostrPairing = ({navigation}: any) => {
{localNpub &&
deviceName &&
partialNonce &&
- !isSendBitcoin && !isSignPSBT && (
+ !isSendBitcoin &&
+ !isSignPSBT && (
{
const peerNum: 1 | 2 = 1;
setScanningForPeer(peerNum);
scanningForPeerRef.current = peerNum; // Update ref immediately
- if (Platform.OS === 'android') {
- BarcodeZxingScan.showQrReader(
- (error: any, data: any) => {
- if (!error && data) {
- handleQRScan(data, peerNum);
- }
- },
- );
- } else {
- setIsQRScannerVisible(true);
- }
+ setIsQRScannerVisible(true);
}}
activeOpacity={0.7}>
{
)}
{/* Peer Connection 2 (Trio only) - Hide when Final Step is shown or in send/sign mode */}
- {isTrio && !showFinalStep && !isSendBitcoin && !isSignPSBT && (
-
-
- Step 3: Third Device
-
-
-
- {peerNpub2 && (
-
- )}
- {peerNpub2 && peerDeviceName2 ? (
-
- {formatConnectionDisplay(
- peerNpub2,
- peerDeviceName2,
- )}
-
- ) : (
- {
- setPeerConnectionDetails2(text);
- handlePeerConnectionInput(text, 2);
- }}
- placeholder="Paste or scan connection details"
- placeholderTextColor={
- theme.colors.textSecondary + '80'
- }
- autoCapitalize="none"
- autoCorrect={false}
- />
- )}
- {peerInputValidating2 && (
-
- ...
-
- )}
- {peerNpub2 && !peerInputValidating2 && (
- clearPeerConnection(2)}
- activeOpacity={0.7}>
+ {isTrio &&
+ !showFinalStep &&
+ !isSendBitcoin &&
+ !isSignPSBT && (
+
+
+ Step 3: Third Device
+
+
+
+ {peerNpub2 && (
-
- )}
- {!peerNpub2 && !peerInputValidating2 && (
- <>
- handlePaste(2)}
- activeOpacity={0.7}>
-
-
+ numberOfLines={1}
+ ellipsizeMode="middle"
+ adjustsFontSizeToFit={true}
+ minimumFontScale={0.7}>
+ {formatConnectionDisplay(
+ peerNpub2,
+ peerDeviceName2,
+ )}
+
+ ) : (
+ {
+ setPeerConnectionDetails2(text);
+ handlePeerConnectionInput(text, 2);
+ }}
+ placeholder="Paste or scan connection details"
+ placeholderTextColor={
+ theme.colors.textSecondary + '80'
+ }
+ autoCapitalize="none"
+ autoCorrect={false}
+ />
+ )}
+ {peerInputValidating2 && (
+
+ ...
+
+ )}
+ {peerNpub2 && !peerInputValidating2 && (
{
- HapticFeedback.light();
- const peerNum: 1 | 2 = 2;
- setScanningForPeer(peerNum);
- scanningForPeerRef.current = peerNum; // Update ref immediately
- if (Platform.OS === 'android') {
- BarcodeZxingScan.showQrReader(
- (error: any, data: any) => {
- if (!error && data) {
- handleQRScan(data, peerNum);
- }
- },
- );
- } else {
- setIsQRScannerVisible(true);
- }
- }}
+ onPress={() => clearPeerConnection(2)}
activeOpacity={0.7}>
- >
+ )}
+ {!peerNpub2 && !peerInputValidating2 && (
+ <>
+ handlePaste(2)}
+ activeOpacity={0.7}>
+
+
+ {
+ HapticFeedback.light();
+ const peerNum: 1 | 2 = 2;
+ setScanningForPeer(peerNum);
+ scanningForPeerRef.current = peerNum; // Update ref immediately
+ setIsQRScannerVisible(true);
+ }}
+ activeOpacity={0.7}>
+
+
+ >
+ )}
+
+ {peerInputError2 && (
+
+
+ ⚠ {peerInputError2}
+
+
)}
- {peerInputError2 && (
-
-
- ⚠ {peerInputError2}
-
-
- )}
-
- )}
+ )}
{/* Prepare Device Section - Hide in send/sign mode */}
- {!isSendBitcoin && !isSignPSBT &&
+ {!isSendBitcoin &&
+ !isSignPSBT &&
!isPreParamsReady &&
localNpub &&
deviceName &&
@@ -5048,7 +4980,8 @@ const MobileNostrPairing = ({navigation}: any) => {
{/* Final Step - Check other devices are prepared */}
- {!isSendBitcoin && !isSignPSBT &&
+ {!isSendBitcoin &&
+ !isSignPSBT &&
isPreParamsReady &&
!mpcDone &&
localNpub &&
@@ -5481,16 +5414,22 @@ const MobileNostrPairing = ({navigation}: any) => {
{
}
activeOpacity={0.8}>
- {((isSendBitcoin || isSignPSBT) || !(isSendBitcoin || isSignPSBT)) && (
+ {(isSendBitcoin ||
+ isSignPSBT ||
+ !(isSendBitcoin || isSignPSBT)) && (
{
/>
)}
- {(isSendBitcoin || isSignPSBT)
+ {isSendBitcoin || isSignPSBT
? (() => {
// Determine if local device is KeyShare1
const localDevice = sendModeDevices.find(
@@ -5520,8 +5461,12 @@ const MobileNostrPairing = ({navigation}: any) => {
const isKeyShare1 =
localDevice?.keyshareLabel === 'KeyShare1';
return isKeyShare1
- ? (isSignPSBT ? 'Start PSBT Signing' : 'Start Co-Signing')
- : (isSignPSBT ? 'Join PSBT Signing' : 'Join Co-Signing');
+ ? isSignPSBT
+ ? 'Start PSBT Signing'
+ : 'Start Co-Signing'
+ : isSignPSBT
+ ? 'Join PSBT Signing'
+ : 'Join Co-Signing';
})()
: (() => {
// For keygen, determine if local npub is first in sorted order
@@ -5626,7 +5571,9 @@ const MobileNostrPairing = ({navigation}: any) => {
{/* Header Text */}
- {isSignPSBT ? 'PSBT Co-Signing' : 'Co-Signing Your Transaction'}
+ {isSignPSBT
+ ? 'PSBT Co-Signing'
+ : 'Co-Signing Your Transaction'}
{/* Subtext */}
@@ -5875,20 +5822,14 @@ const MobileNostrPairing = ({navigation}: any) => {
{/* QR Scanner Modal */}
- {Platform.OS === 'ios' && isQRScannerVisible && (
- setIsQRScannerVisible(false)}>
- handleQRScan(data, scanningForPeer)}
- onClose={() => setIsQRScannerVisible(false)}
- theme={theme}
- />
-
- )}
+ setIsQRScannerVisible(false)}
+ onScan={(data: string) => handleQRScan(data, scanningForPeer)}
+ mode="single"
+ title="Scan Connection QR"
+ subtitle="Point camera at the connection QR from the other device"
+ />
{/* QR Code Modal */}
{
const [confirmPasswordVisible, setConfirmPasswordVisible] = useState(false);
const [passwordStrength, setPasswordStrength] = useState(0);
const [passwordErrors, setPasswordErrors] = useState([]);
-
+
// VPN detection state
const [isVPNConnected, setIsVPNConnected] = useState(false);
const [psbtDetails, setPsbtDetails] = useState<{
@@ -98,7 +106,6 @@ const MobilesPairing = ({navigation}: any) => {
fee: number;
totalInput: number;
totalOutput: number;
- derivePath?: string;
derivePaths?: string[];
} | null>(null);
@@ -118,7 +125,6 @@ const MobilesPairing = ({navigation}: any) => {
selectedCurrency?: string;
spendingHash?: string;
psbtBase64?: string; // For PSBT signing mode
- derivePath?: string; // BIP32 derivation path for PSBT
};
const route = useRoute>();
@@ -297,7 +303,10 @@ const MobilesPairing = ({navigation}: any) => {
route.params.psbtBase64,
);
- if (detailsJson.startsWith('error') || detailsJson.includes('failed')) {
+ if (
+ detailsJson.startsWith('error') ||
+ detailsJson.includes('failed')
+ ) {
dbg('Failed to parse PSBT details:', detailsJson);
setPsbtDetails(null);
return;
@@ -310,7 +319,6 @@ const MobilesPairing = ({navigation}: any) => {
fee: details.fee || 0,
totalInput: details.totalInput || 0,
totalOutput: details.totalOutput || 0,
- derivePath: details.derivePath,
derivePaths: details.derivePaths || [],
});
dbg('PSBT details parsed:', {
@@ -719,8 +727,25 @@ const MobilesPairing = ({navigation}: any) => {
const satoshiFees = `${decoded[2]}`;
const peerShare = `${decoded[3]}`;
- // Derive public key and address for regular BTC sending (not needed for PSBT)
- const path = route.params?.derivePath || getDerivePathForNetwork(net);
+ // Get address type from route params or cache, default to segwit-native
+ const currentAddressType =
+ addressType ||
+ (await LocalCache.getItem('addressType')) ||
+ 'segwit-native';
+ // Check if this is a legacy wallet (created before migration timestamp)
+ const useLegacyPath = isLegacyWallet(ks.created_at);
+ const path = getDerivePathForNetwork(
+ net,
+ currentAddressType,
+ useLegacyPath,
+ );
+ dbg('Deriving path for send:', {
+ net,
+ currentAddressType,
+ useLegacyPath,
+ path,
+ });
+
const btcPub = await BBMTLibNativeModule.derivePubkey(
ks.pub_key,
ks.chain_code_hex,
@@ -731,6 +756,35 @@ const MobilesPairing = ({navigation}: any) => {
net,
addressType,
);
+ dbg('Derived address for send:', {
+ path,
+ btcAddress,
+ addressType,
+ network: net,
+ publicKey: btcPub.substring(0, 20) + '...',
+ });
+
+ // Log warning if address doesn't match expected format for address type
+ if (
+ addressType === 'segwit-native' &&
+ !btcAddress.startsWith('tb1q') &&
+ !btcAddress.startsWith('bc1q')
+ ) {
+ dbg(
+ 'WARNING: Address type is segwit-native but address does not start with tb1q/bc1q:',
+ btcAddress,
+ );
+ } else if (
+ addressType === 'legacy' &&
+ !btcAddress.startsWith('1') &&
+ !btcAddress.startsWith('m') &&
+ !btcAddress.startsWith('n')
+ ) {
+ dbg(
+ 'WARNING: Address type is legacy but address format does not match:',
+ btcAddress,
+ );
+ }
dbg('starting...', {
peerShare,
@@ -895,7 +949,7 @@ const MobilesPairing = ({navigation}: any) => {
}
try {
- HapticFeedback.light();
+ HapticFeedback.medium();
const storedKeyshare = await EncryptedStorage.getItem('keyshare');
if (storedKeyshare) {
@@ -905,15 +959,34 @@ const MobilesPairing = ({navigation}: any) => {
await BBMTLibNativeModule.sha256(password),
);
- // Create friendly filename with date and time (match WalletSettings)
- const now = new Date();
- const month = now.toLocaleDateString('en-US', {month: 'short'});
- const day = now.getDate().toString().padStart(2, '0');
- const year = now.getFullYear();
- const hours = now.getHours().toString().padStart(2, '0');
- const minutes = now.getMinutes().toString().padStart(2, '0');
- const share = json.local_party_key;
- const friendlyFilename = `${share}.${month}${day}.${year}.${hours}${minutes}.share`;
+ // Create filename based on pub_key hash and keyshare number
+ if (!json.pub_key) {
+ Alert.alert('Error', 'Keyshare missing pub_key.');
+ return;
+ }
+
+ // Get SHA256 hash of pub_key and take first 4 characters
+ const pubKeyHash = await BBMTLibNativeModule.sha256(json.pub_key);
+ const hashPrefix = pubKeyHash.substring(0, 4).toLowerCase();
+
+ // Extract keyshare number from label (KeyShare1 -> 1, KeyShare2 -> 2, etc.)
+ const keyshareLabel = getKeyshareLabel(json);
+ let keyshareNumber = '1'; // default
+ if (keyshareLabel) {
+ const match = keyshareLabel.match(/KeyShare(\d+)/);
+ if (match) {
+ keyshareNumber = match[1];
+ }
+ } else if (json.keygen_committee_keys && json.local_party_key) {
+ // Fallback: compute from position in sorted keygen_committee_keys
+ const sortedKeys = [...json.keygen_committee_keys].sort();
+ const index = sortedKeys.indexOf(json.local_party_key);
+ if (index >= 0) {
+ keyshareNumber = String(index + 1);
+ }
+ }
+
+ const friendlyFilename = `${hashPrefix}K${keyshareNumber}.share`;
const tempDir = RNFS.TemporaryDirectoryPath || RNFS.CachesDirectoryPath;
const filePath = `${tempDir}/${friendlyFilename}`;
@@ -931,9 +1004,12 @@ const MobilesPairing = ({navigation}: any) => {
failOnCancel: false,
});
+ // Cleanup temp file (best-effort)
try {
await RNFS.unlink(filePath);
- } catch {}
+ } catch {
+ // ignore cleanup errors
+ }
clearBackupModal();
} else {
Alert.alert('Error', 'Invalid keyshare.');
@@ -1470,21 +1546,29 @@ const MobilesPairing = ({navigation}: any) => {
const netInfo = await NetInfo.fetch();
// Check for VPN on both platforms
let isVPN = false;
-
+
if (netInfo.type === 'vpn') {
isVPN = true;
} else if (Platform.OS === 'android' && netInfo.details) {
// Android: Check details.isVPN if available
const details = netInfo.details as any;
isVPN = details.isVPN === true || false;
- } else if (Platform.OS === 'ios' && netInfo.type === 'other' && netInfo.details) {
+ } else if (
+ Platform.OS === 'ios' &&
+ netInfo.type === 'other' &&
+ netInfo.details
+ ) {
// iOS: Check details.isVPN if available
const details = netInfo.details as any;
isVPN = details.isVPN === true || false;
}
-
+
setIsVPNConnected(isVPN);
- dbg('VPN Status:', {isVPN, type: netInfo.type, details: netInfo.details});
+ dbg('VPN Status:', {
+ isVPN,
+ type: netInfo.type,
+ details: netInfo.details,
+ });
} catch (error) {
dbg('Error checking VPN status:', error);
setIsVPNConnected(false);
@@ -1497,19 +1581,27 @@ const MobilesPairing = ({navigation}: any) => {
// Subscribe to network state changes
const unsubscribe = NetInfo.addEventListener(state => {
let isVPN = false;
-
+
if (state.type === 'vpn') {
isVPN = true;
} else if (Platform.OS === 'android' && state.details) {
const details = state.details as any;
isVPN = details.isVPN === true || false;
- } else if (Platform.OS === 'ios' && state.type === 'other' && state.details) {
+ } else if (
+ Platform.OS === 'ios' &&
+ state.type === 'other' &&
+ state.details
+ ) {
const details = state.details as any;
isVPN = details.isVPN === true || false;
}
-
+
setIsVPNConnected(isVPN);
- dbg('VPN Status Changed:', {isVPN, type: state.type, details: state.details});
+ dbg('VPN Status Changed:', {
+ isVPN,
+ type: state.type,
+ details: state.details,
+ });
});
return () => {
@@ -1523,17 +1615,21 @@ const MobilesPairing = ({navigation}: any) => {
// Re-check VPN status when screen is focused
NetInfo.fetch().then(state => {
let isVPN = false;
-
+
if (state.type === 'vpn') {
isVPN = true;
} else if (Platform.OS === 'android' && state.details) {
const details = state.details as any;
isVPN = details.isVPN === true || false;
- } else if (Platform.OS === 'ios' && state.type === 'other' && state.details) {
+ } else if (
+ Platform.OS === 'ios' &&
+ state.type === 'other' &&
+ state.details
+ ) {
const details = state.details as any;
isVPN = details.isVPN === true || false;
}
-
+
setIsVPNConnected(isVPN);
});
return () => {
@@ -2770,11 +2866,10 @@ const MobilesPairing = ({navigation}: any) => {
resizeMode="contain"
/>
-
- VPN Detected
-
+ VPN Detected
- Please turn off your VPN to ensure a secure local network connection for device pairing.
+ Please turn off your VPN to ensure a secure local network
+ connection for device pairing.
@@ -3898,7 +3993,11 @@ const MobilesPairing = ({navigation}: any) => {
)}
{isSignPSBT && (
-
+
PSBT Ready to Sign
{psbtDetails ? (
@@ -3906,75 +4005,148 @@ const MobilesPairing = ({navigation}: any) => {
-
+
Inputs:
-
+
{psbtDetails.inputs.length}
-
+
Outputs:
-
+
{psbtDetails.outputs.length}
-
-
+
+
Total Input:
-
+
{sat2btcStr(psbtDetails.totalInput)} BTC
-
-
+
+
Total Output:
-
+
{sat2btcStr(psbtDetails.totalOutput)} BTC
-
-
+
+
Fee:
-
+
{sat2btcStr(psbtDetails.fee)} BTC
- {psbtDetails.derivePath && (
-
-
- Derivation Path:
-
- 1 && (
+
- {psbtDetails.derivePath}
-
-
- )}
- {psbtDetails.derivePaths &&
- psbtDetails.derivePaths.length > 1 && (
-
-
- {psbtDetails.derivePaths.length} different paths
+
+ {psbtDetails.derivePaths.length} different
+ paths
)}
@@ -4030,7 +4202,9 @@ const MobilesPairing = ({navigation}: any) => {
{/* Header Text */}
- {isSignPSBT ? 'PSBT Co-Signing' : 'Co-Signing Your Transaction'}
+ {isSignPSBT
+ ? 'PSBT Co-Signing'
+ : 'Co-Signing Your Transaction'}
{/* Subtext */}
diff --git a/screens/PSBTModal.foss.tsx b/screens/PSBTModal.foss.tsx
deleted file mode 100644
index e7fe17c..0000000
--- a/screens/PSBTModal.foss.tsx
+++ /dev/null
@@ -1,1661 +0,0 @@
-import React, {useState, useCallback, useRef, useEffect} from 'react';
-import {
- View,
- Text,
- Modal,
- TouchableOpacity,
- StyleSheet,
- Image,
- ScrollView,
- Platform,
- NativeModules,
- DeviceEventEmitter,
- EmitterSubscription,
-} from 'react-native';
-import DocumentPicker from 'react-native-document-picker';
-import * as RNFS from 'react-native-fs';
-import BarcodeZxingScan from 'rn-barcode-zxing-scan';
-// @ts-ignore - bc-ur types (Buffer polyfill is in polyfills.js)
-import {URDecoder} from '@ngraveio/bc-ur';
-import {dbg, HapticFeedback} from '../utils';
-import {useTheme} from '../theme';
-
-const {BBMTLibNativeModule} = NativeModules;
-
-// PSBT details structure (will be populated when parsing is implemented)
-interface PSBTDetails {
- inputs: Array<{
- txid: string;
- vout: number;
- amount: number; // in satoshis
- }>;
- outputs: Array<{
- address: string;
- amount: number; // in satoshis
- }>;
- fee: number; // in satoshis
- totalInput: number;
- totalOutput: number;
- derivePaths: string[]; // Derivation path for each input (indexed array)
-}
-
-// UR (Uniform Resource) animated QR support for large PSBTs
-
-export interface PSBTLoaderProps {
- btcRate?: number; // BTC to fiat rate
- currencySymbol?: string; // e.g., "$", "€"
- network?: 'mainnet' | 'testnet' | string;
- /**
- * When true, PSBTLoader renders with a full-screen overlay (modal style).
- * When false, it renders as an embedded card without the dimmed backdrop.
- */
- useOverlay?: boolean;
- /**
- * When true, the Cancel button is disabled while no PSBT is loaded.
- * This is used on the dedicated PSBT screen so Cancel only resets state
- * after something has been imported.
- */
- disableCancelWhenEmpty?: boolean;
- /**
- * Optional middle floating button (used on PSBT screen for the lock button).
- */
- middleButton?: React.ReactNode;
- onClose: () => void;
- onSign: (psbtBase64: string) => void;
-}
-
-export interface PSBTModalProps extends PSBTLoaderProps {
- visible: boolean;
-}
-
-export const PSBTLoader: React.FC = ({
- btcRate = 0,
- currencySymbol = '$',
- network,
- useOverlay = true,
- disableCancelWhenEmpty = false,
- middleButton,
- onClose,
- onSign,
-}) => {
- const {theme} = useTheme();
- const [psbtBase64, setPsbtBase64] = useState(null);
- const [psbtDetails, setPsbtDetails] = useState(null);
- const [isLoading, setIsLoading] = useState(false);
- const [error, setError] = useState(null);
- const [isAndroidScanning, setIsAndroidScanning] = useState(false); // For Android continuous scanning modal
-
- // UR (Uniform Resource) animated QR state - using bc-ur library
- const urDecoderRef = useRef(null);
- const isScanningRef = useRef(false); // Track if we're actively scanning (Android)
- const scanSessionIdRef = useRef(0); // Track scan sessions to ignore stale events
- const continuousScanSubscriptionRef = useRef(
- null,
- );
- const [urProgress, setUrProgress] = useState<{
- total: number;
- received: number;
- percentage: number;
- } | null>(null);
-
- // Debug: Log isScanningRef periodically when scanning
- useEffect(() => {
- if (Platform.OS === 'android' && isAndroidScanning) {
- const interval = setInterval(() => {
- dbg(
- 'Android: Periodic check - isScanningRef.current =',
- isScanningRef.current,
- 'isAndroidScanning =',
- isAndroidScanning,
- );
- }, 3000); // Log every 3 seconds when scanning
-
- return () => clearInterval(interval);
- }
- }, [isAndroidScanning]);
-
- // Update progress text overlay in Android zxing activity when urProgress changes
- // This updates the native scanner's progress overlay (not a modal)
- useEffect(() => {
- if (Platform.OS === 'android' && isAndroidScanning && urProgress) {
- const progressPercent = Math.min(100, Math.round(urProgress.received || 0));
- const progressText = `PSBT scanning progress... ${progressPercent}%`;
- BarcodeZxingScan.updateProgressText(progressText);
- } else if (Platform.OS === 'android' && isAndroidScanning && !urProgress) {
- // Show initial message when scanning starts but no progress yet
- BarcodeZxingScan.updateProgressText('Scanning PSBT QR Code...');
- } else if (Platform.OS === 'android' && !isAndroidScanning) {
- // Clear progress text when scanning stops
- BarcodeZxingScan.updateProgressText('');
- }
- }, [urProgress, isAndroidScanning]);
-
- // Reset state when modal closes
- const handleClose = useCallback(() => {
- setPsbtBase64(null);
- setPsbtDetails(null);
- setError(null);
- setIsLoading(false);
- setIsAndroidScanning(false);
- isScanningRef.current = false;
- // Invalidate session to ignore any pending events
- scanSessionIdRef.current = 0;
- // Stop continuous scan on Android
- if (Platform.OS === 'android') {
- BarcodeZxingScan.stopContinuousScan();
- BarcodeZxingScan.updateProgressText('');
- // DON'T remove event listener here - let useEffect cleanup handle it
- // The listener should persist across scans
- }
- urDecoderRef.current = null;
- setUrProgress(null);
- onClose();
- }, [onClose]);
-
- // Parse PSBT and extract details using native module
- const parsePSBT = useCallback(async (base64Data: string) => {
- setIsLoading(true);
- setError(null);
-
- try {
- dbg('PSBT Base64 length:', base64Data.length);
-
- // Use native module to parse PSBT details
- const detailsJson = await BBMTLibNativeModule.parsePSBTDetails(
- base64Data,
- );
- dbg('Native PSBT parse result:', detailsJson.substring(0, 200));
-
- // Check for error response
- if (detailsJson.startsWith('error') || detailsJson.includes('failed')) {
- throw new Error(detailsJson);
- }
-
- // Parse the JSON response
- const parsed = JSON.parse(detailsJson);
-
- const details: PSBTDetails = {
- inputs: parsed.inputs || [],
- outputs: parsed.outputs || [],
- fee: parsed.fee || 0,
- totalInput: parsed.totalInput || 0,
- totalOutput: parsed.totalOutput || 0,
- derivePaths: parsed.derivePaths || parsed.derivePathPerInput || [], // Per-input derivation paths
- };
-
- dbg('PSBT details:', {
- inputCount: details.inputs.length,
- outputCount: details.outputs.length,
- totalInput: details.totalInput,
- totalOutput: details.totalOutput,
- fee: details.fee,
- derivePaths: details.derivePaths,
- });
-
- setPsbtBase64(base64Data);
- setPsbtDetails(details);
- // Use most common derivation path for initial display/navigation
- // The signing functions will extract the correct path per input from the PSBT
- dbg('PSBT loaded successfully.');
- if (details.derivePaths && details.derivePaths.length > 0) {
- dbg('Derivation paths per input:', details.derivePaths);
- }
- } catch (e: any) {
- dbg('Error parsing PSBT:', e.message || e);
- setError(e.message || 'Failed to parse PSBT');
- setPsbtBase64(null);
- setPsbtDetails(null);
- } finally {
- setIsLoading(false);
- }
- }, []);
-
- // Handle file upload
- const handleUploadFile = useCallback(async () => {
- HapticFeedback.light();
- try {
- const result = await DocumentPicker.pick({
- type: [DocumentPicker.types.allFiles],
- copyTo: 'cachesDirectory',
- });
-
- const file = result[0];
- dbg('Selected file:', file.name, file.type);
-
- if (file.fileCopyUri) {
- const fileContent = await RNFS.readFile(
- file.fileCopyUri.replace('file://', ''),
- 'base64',
- );
- await parsePSBT(fileContent);
- } else if (file.uri) {
- // Try reading from original URI
- const uri =
- Platform.OS === 'ios' ? file.uri : file.uri.replace('file://', '');
- const fileContent = await RNFS.readFile(uri, 'base64');
- await parsePSBT(fileContent);
- }
- } catch (e: any) {
- if (!DocumentPicker.isCancel(e)) {
- dbg('Error picking file:', e);
- setError('Failed to load PSBT file');
- }
- }
- }, [parsePSBT]);
-
- // Process UR QR code using bc-ur library (supports fountain codes)
- const processURCode = useCallback(
- (data: string): {isUR: boolean; complete: boolean; psbtBase64?: string} => {
- const lowerData = data.toLowerCase();
-
- // Check if it's a UR code
- if (!lowerData.startsWith('ur:')) {
- return {isUR: false, complete: false};
- }
-
- // Check if it's a PSBT type
- const urType = lowerData.split('/')[0].substring(3);
- if (urType !== 'psbt' && urType !== 'crypto-psbt') {
- dbg('Not a PSBT UR, type:', urType);
- return {isUR: true, complete: false};
- }
-
- try {
- // Initialize decoder if needed
- if (!urDecoderRef.current) {
- urDecoderRef.current = new URDecoder();
- dbg('Created new URDecoder');
- }
-
- // Feed the QR data to the decoder
- // UR decoder uses fountain codes - it can reconstruct from any subset of frames
- // Duplicate frames are automatically handled by the decoder
- urDecoderRef.current.receivePart(data.toLowerCase());
-
- // Get progress
- const progress = urDecoderRef.current.getProgress();
- const estimatedPercentComplete = Math.round(
- urDecoderRef.current.estimatedPercentComplete() * 100,
- );
-
- dbg(`UR progress: ${estimatedPercentComplete}%, received: ${progress}`);
-
- // Update progress - use percentage directly for more accurate display
- // On iOS, ensure we always update even if the value seems the same
- // to trigger re-renders for progress bar
- setUrProgress({
- total: 100,
- received: estimatedPercentComplete,
- percentage: estimatedPercentComplete,
- });
-
- HapticFeedback.light();
-
- // Check if complete
- if (urDecoderRef.current.isComplete()) {
- dbg('UR decoder reports complete');
- if (urDecoderRef.current.isSuccess()) {
- const ur = urDecoderRef.current.resultUR();
- dbg('UR complete! Type:', ur.type);
-
- // Get the CBOR payload
- const cborPayload = ur.decodeCBOR();
- dbg(
- 'CBOR payload type:',
- typeof cborPayload,
- 'length:',
- cborPayload?.length,
- );
-
- // The CBOR payload should be the raw PSBT bytes
- let psbtBytes: Uint8Array;
- if (cborPayload instanceof Uint8Array) {
- psbtBytes = cborPayload;
- } else if (Buffer.isBuffer(cborPayload)) {
- psbtBytes = new Uint8Array(cborPayload);
- } else if (typeof cborPayload === 'object' && cborPayload.data) {
- // Sometimes it comes wrapped
- psbtBytes = new Uint8Array(cborPayload.data);
- } else {
- dbg('Unknown CBOR payload format:', cborPayload);
- return {isUR: true, complete: false};
- }
-
- dbg('PSBT bytes length:', psbtBytes.length);
-
- // Verify PSBT magic bytes (psbt = 0x70736274)
- if (
- psbtBytes.length > 4 &&
- psbtBytes[0] === 0x70 &&
- psbtBytes[1] === 0x73 &&
- psbtBytes[2] === 0x62 &&
- psbtBytes[3] === 0x74
- ) {
- // Convert to base64
- const base64 = btoa(
- String.fromCharCode.apply(null, Array.from(psbtBytes)),
- );
- dbg('PSBT base64 length:', base64.length);
-
- // Reset decoder
- urDecoderRef.current = null;
- setUrProgress(null);
-
- return {isUR: true, complete: true, psbtBase64: base64};
- } else {
- dbg(
- 'Invalid PSBT magic bytes:',
- psbtBytes.slice(0, 4).toString(),
- );
- }
- } else {
- dbg('UR decoding failed:', urDecoderRef.current.resultError());
- }
-
- // Reset on failure
- urDecoderRef.current = null;
- setUrProgress(null);
- }
-
- return {isUR: true, complete: false};
- } catch (e: any) {
- dbg('UR processing error:', e.message || e);
- // Reset decoder on error
- urDecoderRef.current = null;
- setUrProgress(null);
- return {isUR: true, complete: false};
- }
- },
- [],
- );
-
- // Process scanned QR data (handles both plain base64 and UR format)
- const processScannedData = useCallback(
- async (data: string, shouldContinueScanning?: () => void) => {
- dbg(
- 'Processing scanned data:',
- data.substring(0, 50) + '...',
- 'isScanningRef:',
- isScanningRef.current,
- );
-
- // Check if it's UR format
- const urResult = processURCode(data);
-
- if (urResult.isUR) {
- if (urResult.complete && urResult.psbtBase64) {
- // Complete! Stop scanning and parse
- dbg('Android: UR complete, stopping scan');
- setIsAndroidScanning(false);
- isScanningRef.current = false;
- // Reset session ID to invalidate any pending events
- scanSessionIdRef.current = 0;
- // Stop continuous scan on Android
- if (Platform.OS === 'android') {
- BarcodeZxingScan.stopContinuousScan();
- // Clear progress text
- BarcodeZxingScan.updateProgressText('');
- }
- // Decoder is already reset in processURCode when complete
- await parsePSBT(urResult.psbtBase64);
- } else {
- // Not complete yet - scanner stays open for next frame (Android continuous mode)
- dbg(
- 'Android: UR incomplete, continuing scan. isScanningRef:',
- isScanningRef.current,
- );
- // Ensure isScanningRef stays true for continuous scanning
- if (!isScanningRef.current) {
- dbg(
- 'Android: WARNING - isScanningRef was false during UR processing, resetting to true',
- );
- isScanningRef.current = true;
- }
- // On iOS, shouldContinueScanning callback is used
- if (shouldContinueScanning && Platform.OS === 'ios') {
- shouldContinueScanning();
- }
- // On Android, scanner is already open and will emit more events
- }
- return;
- }
-
- // Not UR format - could be:
- // 1. Plain base64 PSBT (single scan) - stop and parse
- // 2. Random QR code (not a PSBT) - ignore and keep scanning in continuous mode
- // Check if it looks like base64 PSBT (starts with cHNidP8BA)
- if (
- data.startsWith('cHNidP8BA') ||
- (data.length > 100 && !data.startsWith('UR:'))
- ) {
- // Looks like a PSBT - stop scanning and parse
- dbg('Android: Found plain PSBT, stopping scan');
- setIsAndroidScanning(false);
- isScanningRef.current = false;
- // Reset session ID to invalidate any pending events
- scanSessionIdRef.current = 0;
- // Stop continuous scan on Android
- if (Platform.OS === 'android') {
- BarcodeZxingScan.stopContinuousScan();
- // Clear progress text
- BarcodeZxingScan.updateProgressText('');
- }
- // Reset decoder for next scan
- urDecoderRef.current = null;
- setUrProgress(null);
- await parsePSBT(data);
- } else {
- // Doesn't look like a PSBT - ignore and keep scanning
- dbg(
- 'Android: Ignoring non-PSBT QR code, continuing scan. isScanningRef:',
- isScanningRef.current,
- );
- // Ensure isScanningRef stays true for continuous scanning
- if (!isScanningRef.current) {
- dbg(
- 'Android: WARNING - isScanningRef was false when ignoring non-PSBT, resetting to true',
- );
- isScanningRef.current = true;
- }
- }
- },
- [processURCode, parsePSBT],
- );
-
- // Helper function to set up the event listener for continuous scanning
- // Called every time we start a scan to ensure listener is fresh and active
- const setupEventListener = useCallback(() => {
- if (Platform.OS !== 'android') {
- return;
- }
-
- // Remove existing listener if any
- if (continuousScanSubscriptionRef.current) {
- dbg('Android: Removing existing event listener before setting up new one');
- continuousScanSubscriptionRef.current.remove();
- continuousScanSubscriptionRef.current = null;
- }
-
- dbg('Android: Setting up EventEmitter listener for continuous scanning');
-
- const subscription = DeviceEventEmitter.addListener(
- 'BarcodeZxingScanContinuous',
- (event: {data?: string; error?: string}) => {
- // Always check current ref value - refs don't have closure issues
- const currentIsScanning = isScanningRef.current;
- const currentSessionId = scanSessionIdRef.current;
- dbg(
- 'Android: Received continuous scan event. isScanningRef:',
- currentIsScanning,
- 'sessionId:',
- currentSessionId,
- 'hasData:',
- !!event.data,
- 'hasError:',
- !!event.error,
- 'eventKeys:',
- Object.keys(event),
- );
-
- if (event.error) {
- // Error or cancellation
- dbg('Android scan error:', event.error);
- setIsAndroidScanning(false);
- isScanningRef.current = false;
- urDecoderRef.current = null;
- setUrProgress(null);
- BarcodeZxingScan.stopContinuousScan();
- BarcodeZxingScan.updateProgressText('');
- return;
- }
-
- if (event.data) {
- dbg('Android: Processing event with data:', event.data.substring(0, 50) + '...');
-
- // CRITICAL: Simplified logic - if we receive an event, process it UNLESS
- // we explicitly know the scan was completed and stopped
- // The session ID helps, but if scanner is open and we get events, process them
-
- // Only reject if BOTH conditions are true: not scanning AND session ID is 0
- // This means the scan was explicitly stopped and we're not in a new session
- if (!currentIsScanning && currentSessionId === 0) {
- // Both false - scan was stopped, ignore this stale event
- dbg(
- 'Android: Received event but scan was stopped (sessionId=0, isScanningRef=false) - ignoring stale event',
- );
- // Don't stop scanner here - it might already be stopped
- return;
- }
-
- // If we get here, we should process the event
- // Re-enable scanning state if needed (handles race conditions)
- if (!currentIsScanning) {
- dbg(
- 'Android: Received event but isScanningRef is false. Enabling scanning (sessionId=' + currentSessionId + ')',
- );
- isScanningRef.current = true;
- setIsAndroidScanning(true);
- }
-
- // CRITICAL: Ensure UR decoder is initialized for this scan session
- // Initialize decoder when we receive the first UR frame
- if (!urDecoderRef.current && event.data.toLowerCase().startsWith('ur:')) {
- dbg('Android: Initializing new UR decoder for scan session');
- urDecoderRef.current = new URDecoder();
- }
-
- // If decoder exists but we're getting non-UR data, reset decoder for new scan
- if (urDecoderRef.current && !event.data.toLowerCase().startsWith('ur:')) {
- dbg('Android: Received non-UR data with existing decoder, resetting decoder for new scan');
- urDecoderRef.current = null;
- setUrProgress(null);
- }
-
- dbg(
- 'Android scanned QR frame:',
- event.data.substring(0, 50) + '...',
- );
- // Process the scanned data - the scanner stays open for next frame
- // Use the latest processScannedData via closure, but also ensure refs are current
- processScannedData(event.data, () => {
- // This callback is called if UR is incomplete - scanner stays open
- dbg(
- 'Android: UR incomplete, scanner stays open for next frame...',
- );
- // Ensure isScanningRef is still true (only if we're still scanning)
- if (isScanningRef.current) {
- // Only reset if we're still supposed to be scanning
- setIsAndroidScanning(true);
- }
- });
- }
- },
- );
-
- continuousScanSubscriptionRef.current = subscription;
- dbg('Android: EventEmitter listener set up successfully');
- }, [processScannedData]);
-
- // Cleanup listener on component unmount
- useEffect(() => {
- return () => {
- if (Platform.OS === 'android' && continuousScanSubscriptionRef.current) {
- dbg('Android: Component unmounting, removing EventEmitter listener');
- continuousScanSubscriptionRef.current.remove();
- continuousScanSubscriptionRef.current = null;
- }
- };
- }, []);
-
- // Android continuous scanning function - uses native continuous scanning
- // The scanner stays open and sends results via EventEmitter until UR is complete
- const startAndroidContinuousScan = useCallback(() => {
- if (isScanningRef.current) {
- dbg('Android: Already scanning, ignoring duplicate call');
- return; // Already scanning
- }
-
- // CRITICAL: Set up event listener every time we start a scan
- // This ensures the listener is fresh and active for each scan session
- dbg('Android: Setting up event listener for new scan');
- setupEventListener();
-
- // CRITICAL: Reset decoder and progress FIRST
- // This ensures clean state for subsequent scans
- urDecoderRef.current = null;
- setUrProgress(null);
- // Clear any progress text from previous scan
- BarcodeZxingScan.updateProgressText('');
-
- // CRITICAL: Increment session ID BEFORE setting scanning flags
- // This ensures events are associated with the correct session
- scanSessionIdRef.current = (scanSessionIdRef.current || 0) + 1;
- const currentSessionId = scanSessionIdRef.current;
- dbg('Android: Starting new scan session, sessionId:', currentSessionId);
-
- // CRITICAL: Set scanning flags BEFORE starting scanner
- // This ensures event listener processes events correctly
- dbg('Android: Setting isScanningRef to true BEFORE starting scanner');
- isScanningRef.current = true;
- setIsAndroidScanning(true);
-
- dbg(
- 'Android: Starting continuous scan for animated QR, sessionId:',
- currentSessionId,
- 'isScanningRef:',
- isScanningRef.current,
- 'isAndroidScanning:',
- true,
- 'listenerActive:',
- !!continuousScanSubscriptionRef.current,
- );
-
- // Use the new continuous scanning API that keeps the activity open
- // The callback is only invoked once to acknowledge scanner start
- // Subsequent results come via EventEmitter
- BarcodeZxingScan.showQrReaderContinuous((scanError: any, data: any) => {
- dbg(
- 'Android: Scanner start callback. Error:',
- scanError,
- 'Data:',
- data,
- 'isScanningRef:',
- isScanningRef.current,
- );
-
- if (scanError) {
- // Error starting scanner
- dbg('Android: Failed to start scanner:', scanError);
- setIsAndroidScanning(false);
- isScanningRef.current = false;
- BarcodeZxingScan.updateProgressText('');
- // Reset session ID on error
- if (scanSessionIdRef.current === currentSessionId) {
- scanSessionIdRef.current = 0;
- }
- return;
- }
-
- // Scanner started successfully - results will come via EventEmitter
- if (data === 'SCANNER_STARTED') {
- // Verify this is still the current session (not a stale callback)
- if (scanSessionIdRef.current !== currentSessionId) {
- dbg('Android: Scanner start callback for stale session, ignoring');
- return;
- }
- dbg(
- 'Android: Scanner started, waiting for QR frames via EventEmitter. sessionId:',
- currentSessionId,
- 'isScanningRef:',
- isScanningRef.current,
- );
- // Double-check that isScanningRef is still true
- if (!isScanningRef.current) {
- dbg(
- 'Android: WARNING - isScanningRef was set to false, resetting to true',
- );
- isScanningRef.current = true;
- setIsAndroidScanning(true);
- }
- // Show progress text now that scanner activity is created
- // Do this after a small delay to ensure activity is ready
- setTimeout(() => {
- BarcodeZxingScan.updateProgressText('Scanning PSBT QR Code...');
- }, 100);
- }
- });
- }, [setupEventListener]); // Include setupEventListener to ensure we have the latest version
-
- // iOS single scan function - uses BarcodeZxingScan (FOSS version)
- const startIOSScan = useCallback(() => {
- // Reset decoder and progress
- urDecoderRef.current = null;
- setUrProgress(null);
-
- // FOSS version: Use BarcodeZxingScan for iOS
- BarcodeZxingScan.showQrReader((scanError: any, data: any) => {
- if (scanError) {
- dbg('iOS scan error:', scanError);
- return;
- }
-
- if (data) {
- dbg('iOS scanned QR:', data.substring(0, 50) + '...');
-
- // Initialize UR decoder if needed
- if (!urDecoderRef.current && data.toLowerCase().startsWith('ur:')) {
- dbg('iOS: Initializing new UR decoder for scan session');
- urDecoderRef.current = new URDecoder();
- }
-
- // Process the scanned data
- processScannedData(data, () => {
- // If UR is incomplete, prompt user to scan again
- if (urDecoderRef.current && !urDecoderRef.current.isComplete()) {
- dbg('iOS: UR incomplete, user needs to scan next frame');
- // Recursively call startIOSScan to scan next frame
- // This allows user to scan multiple frames for animated QR
- setTimeout(() => {
- startIOSScan();
- }, 500); // Small delay before next scan
- }
- });
- }
- });
- }, [processScannedData]);
-
- // Handle QR scan button press
- const handleScanQR = useCallback(() => {
- HapticFeedback.light();
-
- // Reset all state when starting a new scan
- setError(null); // Clear any previous errors
- setIsLoading(false); // Clear loading state
- setPsbtBase64(null); // Clear any previous PSBT data
- setPsbtDetails(null); // Clear PSBT details
-
- if (Platform.OS === 'android') {
- // Ensure any previous scan is fully stopped before starting a new one
- // Always stop any existing scan first to ensure clean state
- if (isScanningRef.current || isAndroidScanning) {
- dbg('handleScanQR: Stopping previous scan before starting new one');
- // Stop the scanner first
- BarcodeZxingScan.stopContinuousScan();
- setIsAndroidScanning(false);
- isScanningRef.current = false;
- // Reset decoder and progress when stopping previous scan
- urDecoderRef.current = null;
- setUrProgress(null);
- BarcodeZxingScan.updateProgressText('');
-
- // Add a small delay to ensure native scanner is fully stopped
- // before starting a new scan (fixes Android subsequent scan issue)
- // NOTE: Don't reset sessionId to 0 here - let the new scan set it
- setTimeout(() => {
- dbg('handleScanQR: Starting new Android scan after cleanup delay');
- startAndroidContinuousScan();
- }, 250); // 250ms delay to ensure cleanup completes
- } else {
- // No previous scan - reset state and start immediately
- urDecoderRef.current = null;
- setUrProgress(null);
- // Don't reset sessionId here - let startAndroidContinuousScan handle it
- dbg('handleScanQR: Starting new Android scan immediately');
- startAndroidContinuousScan();
- }
- } else {
- // iOS: Use BarcodeZxingScan (FOSS version)
- dbg('handleScanQR: Starting iOS scan');
- startIOSScan();
- }
- }, [startAndroidContinuousScan, startIOSScan, isAndroidScanning]);
-
- // Handle sign button
- const handleSign = useCallback(() => {
- HapticFeedback.medium();
- if (psbtBase64) {
- // Pass both PSBT and derivation path to the signing handler
- // The handler will need to accept an object with both values
- onSign(psbtBase64);
- }
- }, [psbtBase64, onSign]);
-
- // Format satoshis to BTC string (compact)
- const formatBTC = (sats: number): string => {
- const btc = sats / 100000000;
- // Show fewer decimals for readability, but keep precision for small amounts
- if (btc >= 0.01) {
- return btc.toFixed(6) + ' BTC';
- }
- return btc.toFixed(8) + ' BTC';
- };
-
- // Format satoshis to fiat string
- const formatFiat = (sats: number): string => {
- if (!btcRate || btcRate === 0) {
- return '';
- }
- const btc = sats / 100000000;
- const fiat = btc * btcRate;
- return `${currencySymbol}${fiat.toLocaleString(undefined, {
- minimumFractionDigits: 2,
- maximumFractionDigits: 2,
- })}`;
- };
-
- const styles = createStyles(theme);
-
- const isCancelDisabled = disableCancelWhenEmpty && !psbtBase64;
-
- return (
-
-
- {/* Header */}
-
-
- Sign • PSBT
-
-
- {network === 'mainnet' ? 'MAINNET' : 'TESTNET'}
-
-
-
-
- {/* Description */}
-
- Import a Partially Signed Bitcoin Transaction (PSBT) from Sparrow or
- another wallet to sign with your keyshare on.
-
-
- {/* Import buttons */}
- {!psbtBase64 && (
-
-
-
- Load PSBT File
-
-
-
-
- Scan PSBT QR
-
-
- )}
-
- {/* Loading indicator */}
- {isLoading && (
-
- Parsing PSBT...
-
- )}
-
- {/* Error message */}
- {error && (
-
- {error}
- {
- setError(null);
- setPsbtBase64(null);
- setPsbtDetails(null);
- }}>
- Try Again
-
-
- )}
-
- {/* PSBT Details - Sparrow-style Visual Flow */}
- {psbtDetails && psbtBase64 && !error && (
-
- {/* Transaction Flow Diagram - Vertical Mobile-Friendly Layout */}
-
- {/* Inputs Section */}
-
- Inputs
- {psbtDetails.inputs.map((input, index) => {
- const derivePath = psbtDetails.derivePaths[index] || 'N/A';
- return (
-
-
-
-
-
-
- {input.txid.slice(0, 8)}...
- {input.txid.slice(-6)}:{input.vout}
-
-
- {derivePath}
-
-
-
-
-
- {formatBTC(input.amount)}
-
- {btcRate > 0 && (
-
- {formatFiat(input.amount)}
-
- )}
-
-
- {/* Flow line connector */}
- {index < psbtDetails.inputs.length - 1 && (
-
- )}
-
- );
- })}
-
-
- {/* Transaction Hub (Center Arrow) */}
-
-
- ↓
-
-
- Transaction
-
-
-
- {/* Outputs Section */}
-
- Outputs
- {psbtDetails.outputs.map((output, index) => {
- // Determine output type: change (likely if small amount), recipient, or fee
- const isLikelyChange =
- output.amount < psbtDetails.totalInput * 0.1; // Heuristic: small outputs are often change
-
- let outputIcon = require('../assets/bitcoin-icon.png');
- let outputType = 'recipient';
-
- if (isLikelyChange) {
- outputIcon = require('../assets/consolidate-icon.png');
- outputType = 'change';
- }
-
- return (
-
-
-
-
-
-
- {output.address.slice(0, 8) +
- '...' +
- output.address.slice(-6)}
-
- {outputType === 'change' && (
- Change
- )}
-
-
-
-
- {formatBTC(output.amount)}
-
- {btcRate > 0 && (
-
- {formatFiat(output.amount)}
-
- )}
-
-
- {/* Flow line connector */}
- {index < psbtDetails.outputs.length - 1 && (
-
- )}
-
- );
- })}
-
- {/* Fee as separate item */}
- {psbtDetails.fee > 0 && (
-
-
-
-
-
- Fee
-
-
-
-
- {formatBTC(psbtDetails.fee)}
-
- {btcRate > 0 && (
-
- {formatFiat(psbtDetails.fee)}
-
- )}
-
-
-
- )}
-
-
-
- {/* Summary Bar */}
-
-
-
- {psbtDetails.inputs.length} input
- {psbtDetails.inputs.length !== 1 ? 's' : ''} →{' '}
- {psbtDetails.outputs.length} output
- {psbtDetails.outputs.length !== 1 ? 's' : ''} •{' '}
- {formatBTC(psbtDetails.totalOutput + psbtDetails.fee)} total
-
- {psbtDetails.derivePaths.length > 0 && (
-
- Path: {psbtDetails.derivePaths.join(', ')}
-
- )}
-
-
-
- {Math.round(psbtBase64.length / 1024)} KB
-
-
-
-
- )}
-
- {/* Action buttons */}
-
-
-
- Cancel
-
-
-
- {middleButton && (
-
- {middleButton}
-
- )}
-
-
-
-
- Co-Sign
-
-
-
-
-
- );
-};
-
-const PSBTModal: React.FC = ({
- visible,
- btcRate = 0,
- currencySymbol = '$',
- network,
- onClose,
- onSign,
-}) => {
- if (!visible) {
- return null;
- }
-
- return (
- {}}>
-
-
- );
-};
-
-const createStyles = (theme: any) =>
- StyleSheet.create({
- modalOverlay: {
- flex: 1,
- backgroundColor: 'rgba(0, 0, 0, 0.85)',
- justifyContent: 'center',
- alignItems: 'center',
- padding: 20,
- },
- modalContent: {
- backgroundColor: theme.colors.background,
- borderRadius: 16,
- padding: 20,
- width: '100%',
- maxWidth: 400,
- maxHeight: '85%',
- // Add shadow for better visibility
- shadowColor: '#000',
- shadowOffset: {width: 0, height: 4},
- shadowOpacity: 0.3,
- shadowRadius: 8,
- elevation: 8,
- },
- // Embedded version used on the dedicated PSBT screen (no overlay)
- // No border/shadow here since it's inside a collapsible section that already has borders
- embeddedContent: {
- backgroundColor: theme.colors.cardBackground,
- borderRadius: 8,
- padding: 16,
- width: '100%',
- overflow: 'hidden',
- },
- headerRow: {
- flexDirection: 'row',
- alignItems: 'center',
- justifyContent: 'space-between',
- marginBottom: 12,
- },
- headerIcon: {
- width: 24,
- height: 24,
- marginRight: 10,
- tintColor: theme.colors.primary,
- },
- headerTitle: {
- fontSize: 20,
- fontWeight: '700',
- color: theme.colors.text,
- flex: 1,
- },
- description: {
- fontSize: 14,
- color: theme.colors.textSecondary,
- lineHeight: 20,
- marginBottom: 16,
- },
- networkBadge: {
- backgroundColor: theme.colors.background,
- paddingHorizontal: 8,
- paddingVertical: 4,
- borderRadius: 8,
- borderWidth: 1,
- borderColor: theme.colors.border,
- },
- networkBadgeText: {
- fontSize: 10,
- fontWeight: '700',
- color: theme.colors.text,
- letterSpacing: 0.5,
- },
- importButtonsContainer: {
- marginBottom: 20,
- },
- importButton: {
- flexDirection: 'row',
- alignItems: 'center',
- justifyContent: 'center',
- backgroundColor: theme.colors.background,
- borderRadius: 12,
- padding: 16,
- borderWidth: 1,
- borderColor: theme.colors.border,
- borderStyle: 'dashed',
- marginBottom: 12,
- },
- importButtonIcon: {
- width: 24,
- height: 24,
- marginRight: 12,
- tintColor: theme.colors.primary,
- },
- importButtonText: {
- fontSize: 16,
- fontWeight: '600',
- color: theme.colors.text,
- },
- loadingContainer: {
- alignItems: 'center',
- padding: 20,
- },
- loadingText: {
- fontSize: 14,
- color: theme.colors.textSecondary,
- },
- errorContainer: {
- backgroundColor: 'rgba(255, 59, 48, 0.1)',
- borderRadius: 12,
- padding: 16,
- marginBottom: 20,
- alignItems: 'center',
- },
- errorText: {
- fontSize: 14,
- color: '#FF3B30',
- textAlign: 'center',
- marginBottom: 12,
- },
- retryButton: {
- backgroundColor: theme.colors.primary,
- paddingHorizontal: 20,
- paddingVertical: 8,
- borderRadius: 8,
- },
- retryButtonText: {
- fontSize: 14,
- fontWeight: '600',
- color: '#FFFFFF',
- },
- detailsContainer: {
- maxHeight: 400,
- marginBottom: 16,
- },
- summaryHeader: {
- flexDirection: 'row',
- justifyContent: 'space-between',
- alignItems: 'center',
- marginBottom: 12,
- },
- summaryLabel: {
- fontSize: 13,
- color: theme.colors.textSecondary,
- },
- psbtSizeBadge: {
- backgroundColor: 'rgba(52, 199, 89, 0.15)',
- paddingHorizontal: 8,
- paddingVertical: 3,
- borderRadius: 6,
- },
- psbtSizeText: {
- fontSize: 11,
- fontWeight: '600',
- color: '#34C759',
- },
- detailsSection: {
- marginBottom: 12,
- },
- detailsSectionTitle: {
- fontSize: 10,
- fontWeight: '700',
- color: theme.colors.textSecondary,
- marginBottom: 6,
- textTransform: 'uppercase',
- letterSpacing: 0.5,
- },
- inputRow: {
- flexDirection: 'row',
- justifyContent: 'space-between',
- alignItems: 'flex-start',
- backgroundColor: theme.colors.card || theme.colors.background,
- borderRadius: 8,
- paddingVertical: 10,
- paddingHorizontal: 10,
- marginBottom: 6,
- },
- inputInfo: {
- flex: 1,
- marginRight: 8,
- },
- inputTxidText: {
- fontSize: 11,
- color: theme.colors.text,
- fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace',
- marginBottom: 4,
- },
- derivePathContainer: {
- flexDirection: 'row',
- alignItems: 'center',
- marginTop: 4,
- },
- derivePathLabel: {
- fontSize: 9,
- color: theme.colors.textSecondary,
- marginRight: 6,
- fontWeight: '600',
- },
- derivePathText: {
- fontSize: 10,
- color: theme.colors.primary,
- fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace',
- flex: 1,
- },
- outputRow: {
- flexDirection: 'row',
- justifyContent: 'space-between',
- alignItems: 'center',
- backgroundColor: theme.colors.card || theme.colors.background,
- borderRadius: 8,
- paddingVertical: 8,
- paddingHorizontal: 10,
- marginBottom: 6,
- },
- addressText: {
- fontSize: 11,
- color: theme.colors.text,
- flex: 1,
- marginRight: 8,
- fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace',
- },
- derivePathSummary: {
- backgroundColor: theme.colors.card || theme.colors.background,
- borderRadius: 8,
- padding: 12,
- marginTop: 8,
- borderWidth: 1,
- borderColor: theme.colors.border,
- },
- derivePathSummaryLabel: {
- fontSize: 10,
- fontWeight: '700',
- color: theme.colors.textSecondary,
- marginBottom: 4,
- textTransform: 'uppercase',
- letterSpacing: 0.5,
- },
- derivePathSummaryText: {
- fontSize: 12,
- color: theme.colors.primary,
- fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace',
- fontWeight: '600',
- },
- derivePathNote: {
- fontSize: 9,
- color: theme.colors.textSecondary,
- marginTop: 4,
- fontStyle: 'italic',
- },
- // Sparrow-style Transaction Flow (Vertical for Mobile)
- transactionFlow: {
- paddingVertical: 12,
- paddingHorizontal: 4,
- },
- flowSection: {
- width: '100%',
- },
- flowSectionTitle: {
- fontSize: 10,
- fontWeight: '700',
- color: theme.colors.textSecondary,
- marginBottom: 12,
- textTransform: 'uppercase',
- letterSpacing: 0.5,
- },
- flowItem: {
- marginBottom: 8,
- },
- flowItemContent: {
- backgroundColor: theme.colors.card || theme.colors.background,
- borderRadius: 8,
- padding: 10,
- borderWidth: 1,
- borderColor: theme.colors.border,
- },
- flowItemHeader: {
- flexDirection: 'row',
- alignItems: 'center',
- marginBottom: 6,
- },
- flowIcon: {
- width: 20,
- height: 20,
- marginRight: 8,
- tintColor: theme.colors.primary,
- },
- flowItemInfo: {
- flex: 1,
- },
- flowItemLabel: {
- fontSize: 11,
- fontWeight: '600',
- color: theme.colors.text,
- fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace',
- marginBottom: 2,
- },
- flowItemPath: {
- fontSize: 9,
- color: theme.colors.primary,
- fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace',
- },
- flowItemType: {
- fontSize: 9,
- color: theme.colors.textSecondary,
- fontStyle: 'italic',
- marginTop: 2,
- },
- flowAmount: {
- alignItems: 'flex-end',
- },
- flowAmountBTC: {
- fontSize: 12,
- fontWeight: '700',
- color: theme.colors.primary,
- },
- flowAmountFiat: {
- fontSize: 9,
- color: theme.colors.textSecondary,
- marginTop: 2,
- },
- flowConnectorVertical: {
- width: 1,
- height: 8,
- backgroundColor: theme.colors.border,
- marginLeft: 14,
- marginVertical: 4,
- },
- transactionHubVertical: {
- alignItems: 'center',
- justifyContent: 'center',
- paddingVertical: 12,
- paddingHorizontal: 8,
- },
- hubArrow: {
- width: 36,
- height: 36,
- borderRadius: 18,
- backgroundColor: theme.colors.primary + '20',
- alignItems: 'center',
- justifyContent: 'center',
- marginBottom: 4,
- },
- hubArrowText: {
- fontSize: 20,
- color: theme.colors.primary,
- fontWeight: '700',
- },
- hubLabel: {
- marginTop: 4,
- },
- hubLabelText: {
- fontSize: 11,
- fontWeight: '600',
- color: theme.colors.textSecondary,
- textTransform: 'uppercase',
- letterSpacing: 0.5,
- },
- summaryBar: {
- flexDirection: 'row',
- justifyContent: 'space-between',
- alignItems: 'center',
- backgroundColor: theme.colors.card || theme.colors.background,
- borderRadius: 12,
- padding: 12,
- marginTop: 16,
- borderWidth: 1,
- borderColor: theme.colors.border,
- },
- summaryBarContent: {
- flex: 1,
- marginRight: 12,
- },
- summaryBarText: {
- fontSize: 12,
- fontWeight: '600',
- color: theme.colors.text,
- marginBottom: 4,
- },
- summaryBarPath: {
- fontSize: 10,
- color: theme.colors.primary,
- fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace',
- },
- summaryBarBadge: {
- backgroundColor: theme.colors.primary + '20',
- borderRadius: 8,
- paddingHorizontal: 10,
- paddingVertical: 6,
- },
- summaryBarBadgeText: {
- fontSize: 10,
- fontWeight: '700',
- color: theme.colors.primary,
- },
- amountContainer: {
- alignItems: 'flex-end',
- },
- amountText: {
- fontSize: 12,
- fontWeight: '600',
- color: theme.colors.primary,
- },
- fiatText: {
- fontSize: 10,
- color: theme.colors.textSecondary,
- marginTop: 1,
- },
- feeTotalContainer: {
- borderTopWidth: 1,
- borderTopColor: theme.colors.border,
- paddingTop: 10,
- },
- feeRow: {
- flexDirection: 'row',
- justifyContent: 'space-between',
- alignItems: 'center',
- marginBottom: 6,
- },
- feeLabel: {
- fontSize: 12,
- color: theme.colors.textSecondary,
- },
- feeValueContainer: {
- alignItems: 'flex-end',
- },
- feeValue: {
- fontSize: 12,
- color: theme.colors.text,
- },
- feeFiatValue: {
- fontSize: 10,
- color: theme.colors.textSecondary,
- },
- totalRow: {
- flexDirection: 'row',
- justifyContent: 'space-between',
- alignItems: 'center',
- paddingTop: 8,
- borderTopWidth: 1,
- borderTopColor: theme.colors.border,
- },
- totalLabel: {
- fontSize: 14,
- fontWeight: '700',
- color: theme.colors.text,
- },
- totalValueContainer: {
- alignItems: 'flex-end',
- },
- totalValue: {
- fontSize: 14,
- fontWeight: '700',
- color: theme.colors.primary,
- },
- totalFiatValue: {
- fontSize: 11,
- color: theme.colors.textSecondary,
- marginTop: 1,
- },
- middleButtonContainer: {
- marginHorizontal: 6,
- justifyContent: 'center',
- alignItems: 'center',
- },
- actionButtonsContainer: {
- flexDirection: 'row',
- marginTop: 20,
- },
- cancelButton: {
- flex: 1,
- backgroundColor: theme.colors.background,
- borderRadius: 12,
- paddingVertical: 14,
- alignItems: 'center',
- borderWidth: 1,
- borderColor: theme.colors.border,
- marginRight: 6,
- },
- cancelButtonDisabled: {
- opacity: 0.5,
- },
- cancelButtonText: {
- fontSize: 16,
- fontWeight: '600',
- color: theme.colors.text,
- },
- cancelButtonTextDisabled: {
- color: theme.colors.textSecondary,
- },
- signButton: {
- flex: 1,
- flexDirection: 'row',
- backgroundColor: theme.colors.primary,
- borderRadius: 12,
- paddingVertical: 14,
- alignItems: 'center',
- justifyContent: 'center',
- marginLeft: 6,
- },
- signButtonDisabled: {
- backgroundColor: theme.colors.border,
- },
- signButtonIcon: {
- width: 20,
- height: 20,
- marginRight: 8,
- tintColor: '#FFFFFF',
- },
- signButtonIconDisabled: {
- tintColor: theme.colors.textSecondary,
- },
- signButtonText: {
- fontSize: 16,
- fontWeight: '600',
- color: '#FFFFFF',
- },
- signButtonTextDisabled: {
- color: theme.colors.textSecondary,
- },
- });
-
-export default PSBTModal;
diff --git a/screens/PSBTModal.tsx b/screens/PSBTModal.tsx
index 66958fc..b39c5c9 100644
--- a/screens/PSBTModal.tsx
+++ b/screens/PSBTModal.tsx
@@ -14,11 +14,7 @@ import {
} from 'react-native';
import DocumentPicker from 'react-native-document-picker';
import * as RNFS from 'react-native-fs';
-import {
- Camera,
- useCameraDevice,
- useCodeScanner,
-} from 'react-native-vision-camera';
+import QRScanner from '../components/QRScanner';
import BarcodeZxingScan from 'rn-barcode-zxing-scan';
// @ts-ignore - bc-ur types (Buffer polyfill is in polyfills.js)
import {URDecoder} from '@ngraveio/bc-ur';
@@ -68,76 +64,6 @@ export interface PSBTModalProps extends PSBTLoaderProps {
visible: boolean;
}
-// QR Scanner component with UR progress display
-const QRScanner = ({styles, device, codeScanner, onClose, urProgress}: any) => {
- if (!device) {
- return (
-
-
- Camera Not Available
-
- Please check camera permissions in Settings
-
-
-
- Close
-
-
- );
- }
-
- const isAnimatedQR = urProgress && urProgress.total > 1;
- // Use percentage directly for more accurate progress display (fixes iOS progress issue)
- const progressPercent = isAnimatedQR
- ? Math.min(
- 100,
- urProgress.percentage ||
- Math.round((urProgress.received / urProgress.total) * 100),
- )
- : 0;
- const isComplete = isAnimatedQR && urProgress.received >= urProgress.total;
-
- return (
-
-
-
-
-
- {isAnimatedQR ? 'Scanning Animated QR...' : 'Scan PSBT QR Code'}
-
-
- {isAnimatedQR
- ? isComplete
- ? 'Processing PSBT...'
- : `Keep scanning animated QR: ${progressPercent}%`
- : 'Position the QR code within the frame'}
-
- {isAnimatedQR && (
-
-
-
- )}
-
-
- Cancel
-
-
- );
-};
export const PSBTLoader: React.FC = ({
btcRate = 0,
@@ -873,36 +799,23 @@ export const PSBTLoader: React.FC = ({
const styles = createStyles(theme);
- // Only use camera hooks on iOS - Android uses BarcodeZxingScan with continuous scanning
- let device;
- let codeScanner;
-
- if (Platform.OS === 'ios') {
- // eslint-disable-next-line react-hooks/rules-of-hooks
- device = useCameraDevice('back');
- // eslint-disable-next-line react-hooks/rules-of-hooks
- codeScanner = useCodeScanner({
- codeTypes: ['qr'],
- onCodeScanned: codes => {
- if (codes.length > 0 && codes[0].value) {
- dbg('Scanned QR (iOS):', codes[0].value.substring(0, 50) + '...');
-
- // CRITICAL: Ensure UR decoder is initialized for this scan session
- // This ensures progress tracking works correctly on iOS
- if (
- !urDecoderRef.current &&
- codes[0].value.toLowerCase().startsWith('ur:')
- ) {
- dbg('iOS: Initializing new UR decoder for scan session');
- urDecoderRef.current = new URDecoder();
- }
+ // Handler for QR scan results (wraps processScannedData for new QRScanner component)
+ const handleQRScan = useCallback((data: string) => {
+ dbg('QR Scanner: Scanned data:', data.substring(0, 50) + '...');
+
+ // CRITICAL: Ensure UR decoder is initialized for this scan session
+ // This ensures progress tracking works correctly
+ if (
+ !urDecoderRef.current &&
+ data.toLowerCase().startsWith('ur:')
+ ) {
+ dbg('QR Scanner: Initializing new UR decoder for scan session');
+ urDecoderRef.current = new URDecoder();
+ }
- // Use processScannedData to handle both plain PSBT and UR format
- processScannedData(codes[0].value);
- }
- },
- });
- }
+ // Use processScannedData to handle both plain PSBT and UR format
+ processScannedData(data);
+ }, [processScannedData]);
return (
@@ -1204,22 +1117,28 @@ export const PSBTLoader: React.FC = ({
- {/* QR Scanner Modal (iOS only) */}
-
-
-
-
- {/* Android scanner opens directly via native activity - no modal needed */}
+ onClose={handleScannerClose}
+ onScan={handleQRScan}
+ mode="continuous"
+ title="Scan PSBT QR Code"
+ subtitle={
+ urProgress && urProgress.total > 1
+ ? urProgress.received >= urProgress.total
+ ? 'Processing PSBT...'
+ : `Keep scanning animated QR: ${Math.min(
+ 100,
+ urProgress.percentage ||
+ Math.round((urProgress.received / urProgress.total) * 100),
+ )}%`
+ : 'Point camera at the PSBT QR code to scan'
+ }
+ showProgress={!!urProgress && urProgress.total > 1}
+ progress={urProgress || undefined}
+ closeButtonText="Cancel"
+ />
);
diff --git a/screens/PSBTScreen.tsx b/screens/PSBTScreen.tsx
index 59c313e..fc25c22 100644
--- a/screens/PSBTScreen.tsx
+++ b/screens/PSBTScreen.tsx
@@ -16,8 +16,8 @@ import {NativeModules} from 'react-native';
import {useTheme} from '../theme';
import {useUser} from '../context/UserContext';
import {HeaderRightButton, HeaderTitle} from '../components/Header';
-import {PSBTLoader} from './PSBTModal.foss';
-import {dbg, HapticFeedback, getDerivePathForNetwork, isLegacyWallet, generateAllOutputDescriptors} from '../utils';
+import {PSBTLoader} from './PSBTModal';
+import {dbg, HapticFeedback, generateAllOutputDescriptors} from '../utils';
import {CommonActions, useRoute, RouteProp} from '@react-navigation/native';
import TransportModeSelector from '../components/TransportModeSelector';
import Clipboard from '@react-native-clipboard/clipboard';
@@ -63,7 +63,6 @@ const PSBTScreen: React.FC<{navigation: any}> = ({navigation}) => {
useState(false);
const [pendingPSBTParams, setPendingPSBTParams] = useState<{
psbtBase64: string;
- derivePath: string;
} | null>(null);
const [signedPsbt, setSignedPsbt] = useState(null);
const [isSignedPSBTModalVisible, setIsSignedPSBTModalVisible] =
@@ -304,20 +303,11 @@ const PSBTScreen: React.FC<{navigation: any}> = ({navigation}) => {
);
// Handle PSBT signing - same logic as WalletHome
+ // Note: The actual signing functions extract derivation paths from PSBT's Bip32Derivation internally
const handlePSBTSign = useCallback(
- async (psbtBase64: string, derivePath?: string) => {
- // Use provided derivation path or default to current address type
- if (!derivePath) {
- const keyshareJSON = await EncryptedStorage.getItem('keyshare');
- if (keyshareJSON) {
- const keyshare = JSON.parse(keyshareJSON);
- const currentAddressType = addressType || 'legacy';
- // Check if this is a legacy wallet (created before migration timestamp)
- const useLegacyPath = isLegacyWallet(keyshare.created_at);
- derivePath = getDerivePathForNetwork(network, currentAddressType, useLegacyPath);
- }
- }
- const psbtDerivePath = derivePath || getDerivePathForNetwork(network, 'legacy', true);
+ async (psbtBase64: string, _derivePath?: string) => {
+ // The actual PSBT signing will extract paths from PSBT's Bip32Derivation field
+ // derivePath parameter is kept for API compatibility but not used
// Check if keyshare supports Nostr (has nostr_npub)
try {
@@ -336,7 +326,6 @@ const PSBTScreen: React.FC<{navigation: any}> = ({navigation}) => {
mode: 'sign_psbt',
addressType,
psbtBase64,
- derivePath: psbtDerivePath,
},
}),
);
@@ -349,19 +338,19 @@ const PSBTScreen: React.FC<{navigation: any}> = ({navigation}) => {
}
// Store params and show transport selector
- setPendingPSBTParams({psbtBase64, derivePath: psbtDerivePath});
+ setPendingPSBTParams({psbtBase64});
setTimeout(() => {
setIsPSBTTransportModalVisible(true);
}, 300);
},
- [network, addressType, navigation],
+ [addressType, navigation],
);
const navigateToPSBTSigning = useCallback(
(transport: 'local' | 'nostr') => {
if (!pendingPSBTParams) return;
- const {psbtBase64, derivePath} = pendingPSBTParams;
+ const {psbtBase64} = pendingPSBTParams;
const routeName =
transport === 'local' ? 'Devices Pairing' : 'Nostr Connect';
@@ -372,7 +361,6 @@ const PSBTScreen: React.FC<{navigation: any}> = ({navigation}) => {
mode: 'sign_psbt',
addressType,
psbtBase64,
- derivePath,
},
}),
);
diff --git a/screens/SendBitcoinModal.foss.tsx b/screens/SendBitcoinModal.foss.tsx
deleted file mode 100644
index 002b6ec..0000000
--- a/screens/SendBitcoinModal.foss.tsx
+++ /dev/null
@@ -1,782 +0,0 @@
-import React, {useState, useCallback, useEffect, useMemo} from 'react';
-import {
- View,
- Text,
- TextInput,
- TouchableOpacity,
- StyleSheet,
- Modal,
- Alert,
- Keyboard,
- TouchableWithoutFeedback,
- KeyboardAvoidingView,
- Platform,
- Image,
- ActivityIndicator,
- NativeModules,
- ScrollView,
- Linking,
-} from 'react-native';
-
-import BarcodeZxingScan from 'rn-barcode-zxing-scan';
-import Clipboard from '@react-native-clipboard/clipboard';
-import debounce from 'lodash/debounce';
-import Big from 'big.js';
-import {dbg, HapticFeedback} from '../utils';
-import {useTheme} from '../theme';
-import LocalCache from '../services/LocalCache';
-import {SafeAreaView} from 'react-native-safe-area-context';
-import {validate as validateBitcoinAddress} from 'bitcoin-address-validation';
-
-const {BBMTLibNativeModule} = NativeModules;
-
-interface SendBitcoinModalProps {
- visible: boolean;
- onClose: () => void;
- onSend: (address: string, amount: Big, estimatedFee: Big, spendingHash: string) => void;
- btcToFiatRate: Big;
- walletBalance: Big;
- walletAddress: string;
- selectedCurrency: string;
-}
-
-const E8 = Big(10).pow(8);
-
-const SendBitcoinModal: React.FC = ({
- visible,
- onClose,
- onSend,
- btcToFiatRate,
- walletBalance,
- walletAddress,
- selectedCurrency,
-}) => {
- const [address, setAddress] = useState('');
- const [btcAmount, setBtcAmount] = useState(Big(0));
- const [inBtcAmount, setInBtcAmount] = useState('');
- const [inUsdAmount, setInUsdAmount] = useState('');
- const [estimatedFee, setEstimatedFee] = useState(null);
- const [isCalculatingFee, setIsCalculatingFee] = useState(false);
- const [spendingHash, setSpendingHash] = useState('');
- const [_activeInput, setActiveInput] = useState<'btc' | 'usd' | null>(null);
- const [feeStrategy, setFeeStrategy] = useState('eco');
-
- const {theme} = useTheme();
-
- const styles = StyleSheet.create({
- feeStrategyContainer: {
- marginBottom: 10,
- },
- feeStrategyButton: {
- backgroundColor: '#e9ecef',
- paddingVertical: 8,
- paddingHorizontal: 12,
- borderRadius: 16,
- marginRight: 8,
- borderWidth: 1,
- borderColor: theme.colors.border,
- },
- feeStrategyButtonSelected: {
- backgroundColor: theme.colors.primary,
- borderColor: theme.colors.primary,
- },
- feeStrategyText: {
- fontSize: 14,
- color: '#495057',
- fontWeight: '600',
- },
- feeStrategyTextSelected: {
- color: '#fff',
- },
- label: {
- fontSize: 14,
- fontWeight: '600',
- marginBottom: 8,
- color: '#7f8c8d',
- },
- modalBackdrop: {
- flex: 1,
- justifyContent: 'center',
- alignItems: 'center',
- backgroundColor: 'rgba(0, 0, 0, 0.8)',
- },
- modalContainer: {
- width: '90%',
- backgroundColor: theme.colors.background,
- borderRadius: 10,
- padding: 20,
- },
- header: {
- flexDirection: 'row',
- justifyContent: 'space-between',
- alignItems: 'center',
- width: '100%',
- marginBottom: 20,
- paddingHorizontal: 4,
- },
- titleContainer: {
- flexDirection: 'row',
- alignItems: 'center',
- gap: 8,
- flex: 1,
- },
- bitcoinLogo: {
- width: 24,
- height: 24,
- resizeMode: 'contain',
- },
- title: {
- fontSize: 20,
- fontWeight: 'bold',
- color: theme.colors.text,
- flex: 1,
- },
- closeButton: {
- width: 30,
- height: 30,
- },
- closeButtonText: {
- fontSize: 16,
- color: theme.colors.text,
- fontWeight: '600',
- textAlign: 'center',
- verticalAlign: 'middle',
- lineHeight: 30,
- },
- input: {
- borderWidth: 1,
- borderColor: theme.colors.secondary,
- borderRadius: 8,
- padding: 12,
- fontSize: 16,
- maxHeight: 50,
- backgroundColor: '#FFF',
- marginBottom: 10,
- },
- inputWithIcons: {
- position: 'relative',
- marginBottom: 20,
- marginTop: 20,
- },
- inputAddressWithIcons: {
- borderWidth: 1,
- borderColor: theme.colors.secondary,
- borderRadius: 8,
- padding: 12,
- paddingRight: 80,
- minHeight: 48,
- maxHeight: 120,
- fontSize: 14,
- backgroundColor: '#FFF',
- textAlignVertical: 'top',
- fontFamily: Platform.select({ios: 'Menlo', android: 'monospace'}) as any,
- },
- iconImage: {
- width: 24,
- height: 24,
- },
- pasteIconContainer: {
- position: 'absolute',
- top: 12,
- right: 40,
- },
- qrIconContainer: {
- position: 'absolute',
- top: 12,
- right: 10,
- },
- labelContainer: {
- flexDirection: 'row',
- justifyContent: 'space-between',
- alignItems: 'center',
- marginBottom: 5,
- },
- maxText: {
- color: theme.colors.accent,
- fontSize: 14,
- fontWeight: 'bold',
- marginBottom: 10,
- textDecorationLine: 'underline',
- },
- inputContainer: {
- marginBottom: 0,
- },
- inputLabel: {
- fontSize: 14,
- fontWeight: '600',
- marginBottom: 8,
- color: '#7f8c8d',
- },
- inputError: {
- borderColor: theme.colors.danger || '#DC3545',
- },
- errorText: {
- fontSize: 12,
- color: theme.colors.danger || '#DC3545',
- marginTop: -8,
- marginBottom: 8,
- marginLeft: 4,
- },
- feeContainer: {
- marginTop: 15,
- padding: 10,
- backgroundColor: '#f8f9fa',
- borderRadius: 8,
- borderWidth: 1,
- borderColor: theme.colors.secondary,
- },
- feeLabel: {
- fontSize: 14,
- fontWeight: '600',
- color: '#7f8c8d',
- },
- feeInfoContainer: {
- marginTop: 5,
- },
- feeAmount: {
- fontSize: 16,
- fontWeight: 'bold',
- color: theme.colors.text,
- },
- feeCalculating: {
- marginLeft: 10,
- color: '#7f8c8d',
- fontSize: 14,
- },
- feeAmountContainer: {
- flexDirection: 'row',
- alignItems: 'center',
- justifyContent: 'space-between',
- },
- feeLoadingContainer: {
- flexDirection: 'row',
- alignItems: 'center',
- justifyContent: 'center',
- marginTop: 5,
- },
- feeAmountUsd: {
- fontSize: 14,
- color: '#7f8c8d',
- },
- sendCancelButtons: {
- flexDirection: 'row',
- justifyContent: 'space-between',
- marginTop: 20,
- },
- sendButton: {
- flex: 1,
- backgroundColor: theme.colors.primary,
- padding: 15,
- borderRadius: 8,
- alignItems: 'center',
- marginRight: 10,
- },
- cancelButton: {
- flex: 1,
- backgroundColor: theme.colors.secondary,
- padding: 15,
- borderRadius: 8,
- alignItems: 'center',
- marginLeft: 10,
- },
- buttonText: {
- color: '#fff',
- fontSize: 16,
- fontWeight: 'bold',
- },
- disabledButton: {
- opacity: 0.5,
- },
- scannerContainer: {
- flex: 1,
- backgroundColor: 'black',
- },
- qrFrame: {
- position: 'absolute',
- borderWidth: 2,
- borderColor: 'white',
- width: 250,
- height: 250,
- alignSelf: 'center',
- top: '25%',
- },
- closeScannerButton: {
- position: 'absolute',
- top: 50,
- right: 20,
- backgroundColor: theme.colors.accent,
- padding: 10,
- borderRadius: 50,
- },
- closeScannerButtonText: {
- color: '#fff',
- fontWeight: 'bold',
- },
- cameraNotFound: {
- flex: 1,
- justifyContent: 'center',
- alignItems: 'center',
- },
- // Setup Guide Hint Styles
- setupGuideHint: {
- marginTop: 12,
- alignItems: 'center',
- },
- setupGuideHintTouchable: {
- paddingVertical: 8,
- paddingHorizontal: 12,
- borderRadius: 8,
- },
- setupGuideHintRow: {
- flexDirection: 'row',
- alignItems: 'center',
- gap: 8,
- },
- setupGuideHintIcon: {
- width: 16,
- height: 16,
- tintColor: theme.colors.primary,
- },
- setupGuideHintText: {
- fontSize: 13,
- color: theme.colors.primary,
- fontWeight: '500',
- textDecorationLine: 'underline',
- textDecorationColor: theme.colors.primary + '80',
- },
- });
-
- const feeStrategies = [
- {label: 'Economy', value: 'eco'},
- {label: 'Top Priority', value: 'top'},
- {label: '30 Min', value: '30m'},
- {label: '1 Hour', value: '1hr'},
- ];
-
- const formatUSD = (price: number) =>
- new Intl.NumberFormat('en-US', {
- style: 'decimal',
- minimumFractionDigits: 2,
- maximumFractionDigits: 2,
- }).format(price);
-
- const getFee = useCallback(
- async (addr: string, amt: string) => {
- if (!addr || !amt || btcAmount.eq(0)) {
- setEstimatedFee(null);
- return;
- }
-
- const amount = Big(amt);
-
- if (amount.gt(walletBalance) || !walletBalance) {
- setEstimatedFee(null);
- return;
- }
-
- setIsCalculatingFee(true);
- const satoshiAmount = amount.times(1e8).toFixed(0);
- BBMTLibNativeModule.spendingHash(
- walletAddress,
- addr,
- satoshiAmount,
- )
- .then((hash: string) => {
- setSpendingHash(hash);
- dbg('got spending hash:', hash);
- BBMTLibNativeModule.estimateFees(
- walletAddress,
- addr,
- satoshiAmount,
- )
- .then((fee: string) => {
- if (fee && typeof fee === 'string') {
- // Check if the response contains an error message
- if (
- fee.includes('failed') ||
- fee.includes('error') ||
- fee.includes('[')
- ) {
- dbg('Fee estimation API returned error:', fee);
- setEstimatedFee(null);
- return;
- }
-
- // Try to parse the fee as a valid number
- try {
- const feeNumber = parseFloat(fee);
- if (isNaN(feeNumber) || feeNumber <= 0) {
- dbg('Invalid fee amount received:', fee);
- setEstimatedFee(null);
- return;
- }
-
- dbg('got fees:', fee);
- const feeAmt = Big(feeNumber.toString());
- setEstimatedFee(feeAmt);
- if (Big(inBtcAmount).eq(walletBalance)) {
- setInBtcAmount(
- walletBalance.minus(feeAmt.div(1e8)).toString(),
- );
- }
- } catch (parseError) {
- dbg('Failed to parse fee amount:', fee, parseError);
- setEstimatedFee(null);
- }
- } else {
- dbg('No fee data received from API');
- setEstimatedFee(null);
- }
- })
- .catch((e: any) => {
- dbg('Fee estimation failed:', e);
- setEstimatedFee(null);
- // Only show alert for network/API errors, not parsing errors
- if (e.message && !e.message.includes('Invalid number')) {
- Alert.alert(
- 'Fee Estimation Error',
- 'Unable to estimate transaction fee. Please try again later.',
- );
- }
- })
- .finally(() => {
- setIsCalculatingFee(false);
- });
- })
- .catch((e: any) => {
- dbg('Spending hash failed:', e);
- setIsCalculatingFee(false);
- setEstimatedFee(null);
- });
- },
- [btcAmount, walletBalance, walletAddress, inBtcAmount],
- );
-
- const debouncedGetFee = useMemo(() => debounce(getFee, 1000), [getFee]);
-
- useEffect(() => {
- const initFee = async () => {
- const feeOption = await LocalCache.getItem('feeStrategy');
- const defaultFee = feeOption && feeOption !== 'min' ? feeOption : 'eco';
- setFeeStrategy(defaultFee);
- BBMTLibNativeModule.setFeePolicy(defaultFee);
- dbg('using fee strategy', defaultFee);
- };
- initFee();
- }, []);
-
- useEffect(() => {
- // Only trigger fee estimation if we have a valid address and non-zero amount
- if (address && btcAmount && btcAmount.gt(0) && validateBitcoinAddress(address)) {
- debouncedGetFee(address, btcAmount.toString());
- } else {
- // Clear fee if conditions aren't met
- setEstimatedFee(null);
- }
- }, [address, btcAmount, debouncedGetFee, feeStrategy]);
-
- const pasteAddress = useCallback(async () => {
- HapticFeedback.light();
- const text = await Clipboard.getString();
- setAddress(text);
- }, []);
-
- const handleBtcChange = (text: string) => {
- setActiveInput('btc');
- setInBtcAmount(text);
- try {
- const btc = Big(text || 0);
- setBtcAmount(btc);
- // Always update USD amount when BTC changes (no need to check activeInput)
- setInUsdAmount(btc.mul(btcToFiatRate).toFixed(2));
- } catch {
- dbg('Invalid BTC input:', text);
- }
- };
-
- const handleUsdChange = (text: string) => {
- setActiveInput('usd');
- setInUsdAmount(text);
- try {
- const usd = Big(text || 0);
- const calculatedBtc = usd.div(btcToFiatRate);
- // Always update BTC amount when USD changes (no need to check activeInput)
- setBtcAmount(calculatedBtc);
- setInBtcAmount(calculatedBtc.toFixed(8));
- } catch {
- dbg('Invalid USD input:', text);
- }
- };
-
- const handleMaxClick = () => {
- HapticFeedback.medium();
- setBtcAmount(walletBalance);
- setInBtcAmount(walletBalance.toFixed(8));
- setInUsdAmount(walletBalance.times(btcToFiatRate).toFixed(2));
- };
-
- const handleFeeStrategyChange = (value: string) => {
- HapticFeedback.selection();
- setFeeStrategy(value);
- dbg('setting fee strategy to', value);
- BBMTLibNativeModule.setFeePolicy(value);
- LocalCache.setItem('feeStrategy', value);
- };
-
- const handleSendClick = () => {
- // Client-side Bitcoin address validation
- if (!address || !validateBitcoinAddress(address)) {
- Alert.alert('Error', 'Please enter a valid Bitcoin address');
- return;
- }
-
- if (!estimatedFee) {
- Alert.alert('Error', 'Please wait for fee estimation');
- return;
- }
- const feeBTC = estimatedFee.div(1e8);
- const totalAmount = Big(inBtcAmount).add(feeBTC);
- if (totalAmount.gt(walletBalance)) {
- Alert.alert('Error', 'Total amount including fee exceeds wallet balance');
- return;
- }
- HapticFeedback.heavy();
- onSend(address, Big(inBtcAmount).times(1e8), estimatedFee, spendingHash);
- };
-
- // Check if amount exceeds balance
- const amountExceedsBalance = btcAmount.gt(0) && btcAmount.gt(walletBalance);
-
- const renderFeeSection = () => {
- if (!address || !btcAmount) {
- return null;
- }
- return (
-
- {isCalculatingFee ? (
-
-
- Calculating...
-
- ) : estimatedFee ? (
-
-
- Network Fee:
-
- {feeStrategies.map(strategy => (
- handleFeeStrategyChange(strategy.value)}>
-
- {strategy.label}
-
-
- ))}
-
-
-
-
- {estimatedFee.div(E8).toFixed(8)} BTC
-
-
- ({selectedCurrency}{' '}
- {formatUSD(
- estimatedFee.div(E8).times(btcToFiatRate).toNumber(),
- )}
- )
-
-
-
- ) : null}
-
- );
- };
-
- return (
-
-
-
-
-
-
-
-
-
- Send Bitcoin
-
-
- ✖️
-
-
-
-
-
-
-
- {
- HapticFeedback.light();
- if (Platform.OS === 'android') {
- BarcodeZxingScan.showQrReader(
- (error: any, data: any) => {
- if (!error) {
- setAddress(data);
- }
- },
- );
- }
- }}
- style={styles.qrIconContainer}>
-
-
-
-
-
-
- Amount in BTC (₿)
-
- Max
-
-
- setActiveInput('btc')}
- keyboardType="decimal-pad"
- />
- {amountExceedsBalance && (
-
- Amount exceeds wallet balance ({walletBalance.toFixed(8)} BTC)
-
- )}
-
-
-
-
- Amount in {selectedCurrency} ($)
-
- setActiveInput('usd')}
- onChangeText={handleUsdChange}
- keyboardType="decimal-pad"
- />
-
-
- {renderFeeSection()}
-
- {/* Setup Guide Hint */}
-
- {
- HapticFeedback.medium();
- const url =
- 'https://x.com/boldbtcwallet/status/1988332367489237160';
- Linking.openURL(url).catch(err => {
- Alert.alert('Error', 'Unable to open the video link');
- dbg('Error opening URL:', err);
- });
- }}
- activeOpacity={0.7}>
-
-
-
- 🎥 Watch Send Bitcoin video guide →
-
-
-
-
-
-
-
- Send
-
- {
- HapticFeedback.light();
- onClose();
- }}
- activeOpacity={0.7}>
- Cancel
-
-
-
-
-
-
-
-
- );
-};
-
-export default SendBitcoinModal;
diff --git a/screens/SendBitcoinModal.tsx b/screens/SendBitcoinModal.tsx
index aed9e2e..550df83 100644
--- a/screens/SendBitcoinModal.tsx
+++ b/screens/SendBitcoinModal.tsx
@@ -17,16 +17,11 @@ import {
ScrollView,
Linking,
} from 'react-native';
-import {
- Camera,
- useCameraDevice,
- useCodeScanner,
-} from 'react-native-vision-camera';
-import BarcodeZxingScan from 'rn-barcode-zxing-scan';
+import QRScanner from '../components/QRScanner';
import Clipboard from '@react-native-clipboard/clipboard';
import debounce from 'lodash/debounce';
import Big from 'big.js';
-import {dbg, HapticFeedback} from '../utils';
+import {dbg, HapticFeedback, decodeSendBitcoinQR} from '../utils';
import {useTheme} from '../theme';
import LocalCache from '../services/LocalCache';
import {SafeAreaView} from 'react-native-safe-area-context';
@@ -51,30 +46,6 @@ interface SendBitcoinModalProps {
const E8 = Big(10).pow(8);
-const QRScanner = ({styles, device, codeScanner, onClose}: any) => {
- if (!device) {
- return Camera Not Found;
- }
- return (
-
-
-
-
- Close
-
-
- );
-};
-
const SendBitcoinModal: React.FC = ({
visible,
onClose,
@@ -322,36 +293,6 @@ const SendBitcoinModal: React.FC = ({
disabledButton: {
opacity: 0.5,
},
- scannerContainer: {
- flex: 1,
- backgroundColor: 'black',
- },
- qrFrame: {
- position: 'absolute',
- borderWidth: 2,
- borderColor: 'white',
- width: 250,
- height: 250,
- alignSelf: 'center',
- top: '25%',
- },
- closeScannerButton: {
- position: 'absolute',
- top: 50,
- right: 20,
- backgroundColor: theme.colors.accent,
- padding: 10,
- borderRadius: 50,
- },
- closeScannerButtonText: {
- color: '#fff',
- fontWeight: 'bold',
- },
- cameraNotFound: {
- flex: 1,
- justifyContent: 'center',
- alignItems: 'center',
- },
// Setup Guide Hint Styles
setupGuideHint: {
marginTop: 12,
@@ -381,24 +322,6 @@ const SendBitcoinModal: React.FC = ({
},
});
- // Only use camera hooks on iOS - Android uses BarcodeZxingScan
- let device;
- let codeScanner;
-
- if (Platform.OS === 'ios') {
- // eslint-disable-next-line react-hooks/rules-of-hooks
- device = useCameraDevice('back');
- // eslint-disable-next-line react-hooks/rules-of-hooks
- codeScanner = useCodeScanner({
- codeTypes: ['qr'],
- onCodeScanned: codes => {
- if (codes.length > 0) {
- setAddress(codes[0].value!!);
- setIsScannerVisible(false);
- }
- },
- });
- }
const feeStrategies = [
{label: 'Economy', value: 'eco'},
@@ -469,9 +392,11 @@ const SendBitcoinModal: React.FC = ({
const feeAmt = Big(feeNumber.toString());
setEstimatedFee(feeAmt);
if (Big(inBtcAmount).eq(walletBalance)) {
- setInBtcAmount(
- walletBalance.minus(feeAmt.div(1e8)).toString(),
- );
+ // When MAX is clicked, adjust amount to account for fee
+ const adjustedAmount = walletBalance.minus(feeAmt.div(1e8));
+ setInBtcAmount(adjustedAmount.toFixed(8));
+ setBtcAmount(adjustedAmount);
+ setInUsdAmount(adjustedAmount.times(btcToFiatRate).toFixed(2));
}
} catch (parseError) {
dbg('Failed to parse fee amount:', fee, parseError);
@@ -503,7 +428,7 @@ const SendBitcoinModal: React.FC = ({
setEstimatedFee(null);
});
},
- [btcAmount, walletBalance, walletAddress, inBtcAmount],
+ [btcAmount, walletBalance, walletAddress, inBtcAmount, btcToFiatRate],
);
const debouncedGetFee = useMemo(() => debounce(getFee, 1000), [getFee]);
@@ -594,6 +519,60 @@ const SendBitcoinModal: React.FC = ({
setInUsdAmount(walletBalance.times(btcToFiatRate).toFixed(2));
};
+ // Handle QR scan - supports both regular addresses and send bitcoin QR format
+ const handleQRScan = useCallback((qrData: string) => {
+ if (!qrData || !qrData.trim()) {
+ return;
+ }
+
+ // Check if it's a send bitcoin QR format (address|amount|fee|hash)
+ const decoded = decodeSendBitcoinQR(qrData) as {
+ toAddress: string;
+ amountSats: string;
+ feeSats: string;
+ spendingHash?: string;
+ } | null;
+ if (decoded && decoded.toAddress && decoded.amountSats && decoded.feeSats) {
+ // It's a send bitcoin QR format - populate all fields
+ if (!validateBitcoinAddress(decoded.toAddress)) {
+ Alert.alert('Invalid Address', 'The scanned QR code contains an invalid Bitcoin address.');
+ return;
+ }
+
+ const amountSats = Big(decoded.amountSats);
+ const feeSats = Big(decoded.feeSats);
+ const amountBTC = amountSats.div(1e8);
+
+ if (amountSats.lte(0) || feeSats.lte(0)) {
+ Alert.alert('Invalid Amount', 'The scanned QR code contains invalid amount or fee values.');
+ return;
+ }
+
+ // Populate form fields
+ setAddress(decoded.toAddress);
+ setBtcAmount(amountBTC);
+ setInBtcAmount(amountBTC.toFixed(8));
+ setInUsdAmount(amountBTC.times(btcToFiatRate).toFixed(2));
+ setSpendingHash(decoded.spendingHash || '');
+
+ // Set the fee (will be validated when fee estimation runs)
+ // Note: The fee from QR might not match current network conditions,
+ // but we'll let the fee estimation handle that
+
+ Alert.alert(
+ 'Transaction Details Loaded',
+ `Address and amount have been filled from the QR code.\n\nAddress: ${decoded.toAddress.substring(0, 10)}...\nAmount: ${amountBTC.toFixed(8)} BTC\n\nPlease review and confirm.`,
+ );
+ } else {
+ // It's a regular Bitcoin address - just set the address
+ if (validateBitcoinAddress(qrData.trim())) {
+ setAddress(qrData.trim());
+ } else {
+ Alert.alert('Invalid QR Code', 'The scanned QR code is not a valid Bitcoin address or send bitcoin data.');
+ }
+ }
+ }, [btcToFiatRate]);
+
const handleFeeStrategyChange = (value: string) => {
HapticFeedback.selection();
setFeeStrategy(value);
@@ -734,17 +713,7 @@ const SendBitcoinModal: React.FC = ({
{
HapticFeedback.light();
- if (Platform.OS === 'android') {
- BarcodeZxingScan.showQrReader(
- (error: any, data: any) => {
- if (!error) {
- setAddress(data);
- }
- },
- );
- } else {
- setIsScannerVisible(true);
- }
+ setIsScannerVisible(true);
}}
style={styles.qrIconContainer}>
= ({
- setIsScannerVisible(false)}>
- setIsScannerVisible(false)}
- />
-
+ onClose={() => setIsScannerVisible(false)}
+ onScan={handleQRScan}
+ mode="single"
+ title="Scan QR Code"
+ subtitle="Point camera at the QR data"
+ />
diff --git a/screens/ShowcaseScreen.tsx b/screens/ShowcaseScreen.tsx
index ca3783f..d122882 100644
--- a/screens/ShowcaseScreen.tsx
+++ b/screens/ShowcaseScreen.tsx
@@ -1083,7 +1083,7 @@ const ShowcaseScreen = ({navigation}: any) => {
styles.modeOptionTitle,
{color: theme.colors.primary},
]}>
- Basic
+ Duo
@@ -1168,7 +1168,7 @@ const ShowcaseScreen = ({navigation}: any) => {
styles.modeOptionTitle,
{color: theme.colors.primary},
]}>
- Flexi
+ Trio
@@ -1185,7 +1185,7 @@ const ShowcaseScreen = ({navigation}: any) => {
{selectedMode === 'duo' ? (
- Basic (2/2)
+ Duo (2/2)
: two devices needed for wallet setup. both of them must
approve transactions when spending funds.
@@ -1193,7 +1193,7 @@ const ShowcaseScreen = ({navigation}: any) => {
) : (
- Flexi (2/3)
+ Trio (2/3)
: three devices needed for wallet setup. any 2 of them
must approve transactions when spending funds.
diff --git a/screens/WalletHome.tsx b/screens/WalletHome.tsx
index e010c1c..d18d165 100644
--- a/screens/WalletHome.tsx
+++ b/screens/WalletHome.tsx
@@ -12,6 +12,7 @@ import {
DeviceEventEmitter,
Linking,
} from 'react-native';
+import QRScanner from '../components/QRScanner';
import {
useFocusEffect,
useNavigation,
@@ -28,7 +29,7 @@ import {CommonActions} from '@react-navigation/native';
import Big from 'big.js';
import ReceiveModal from './ReceiveModal';
import SignedPSBTModal from './SignedPSBTModal';
-import PSBTModal from './PSBTModal.foss';
+import PSBTModal from './PSBTModal';
import KeyshareModal from '../components/KeyshareModal';
import QRCodeModal from '../components/QRCodeModal';
import {
@@ -42,7 +43,9 @@ import {
getDerivePathForNetwork,
isLegacyWallet,
generateAllOutputDescriptors,
+ decodeSendBitcoinQR,
} from '../utils';
+import {validate as validateBitcoinAddress} from 'bitcoin-address-validation';
import {useTheme} from '../theme';
import {WalletService} from '../services/WalletService';
import WalletSkeleton from '../components/WalletSkeleton';
@@ -85,14 +88,13 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => {
useState(false);
const [pendingPSBTParams, setPendingPSBTParams] = useState<{
psbtBase64: string;
- derivePath: string;
} | null>(null);
const [btcPrice, setBtcPrice] = useState('');
const [btcRate, setBtcRate] = useState(0);
const [balanceBTC, setBalanceBTC] = useState('0.00000000');
const [balanceFiat, setBalanceFiat] = useState('0');
const [party, setParty] = useState('');
- const [isBlurred, setIsBlurred] = useState(true);
+ const [isBlurred, setIsBlurred] = useState(false);
const [isReceiveModalVisible, setIsReceiveModalVisible] = useState(false);
const [isSignedPSBTModalVisible, setIsSignedPSBTModalVisible] =
useState(false);
@@ -117,6 +119,8 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => {
const [isCurrencySelectorVisible, setIsCurrencySelectorVisible] =
useState(false);
const [selectedCurrency, setSelectedCurrency] = useState('');
+ const [isQRScannerVisible, setIsQRScannerVisible] = useState(false);
+ const [scannedFromQR, setScannedFromQR] = useState(false); // Track if data came from QR scan
const [priceData, setPriceData] = useState<{[key: string]: number}>({});
const [segwitCompatibleAddress, setSegwitCompatibleAddress] =
React.useState('');
@@ -393,10 +397,15 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => {
if (jks) {
const ks = JSON.parse(jks);
// Get current address type for derivation path
- const currentAddressType = (await LocalCache.getItem('addressType')) || 'legacy';
+ const currentAddressType =
+ (await LocalCache.getItem('addressType')) || 'segwit-native';
// Check if this is a legacy wallet (created before migration timestamp)
const useLegacyPath = isLegacyWallet(ks.created_at);
- const path = getDerivePathForNetwork(network, currentAddressType, useLegacyPath);
+ const path = getDerivePathForNetwork(
+ network,
+ currentAddressType,
+ useLegacyPath,
+ );
btcPub = await BBMTLibNativeModule.derivePubkey(
ks.pub_key,
ks.chain_code_hex,
@@ -451,7 +460,9 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => {
// Get the current address type from cache or state
const currentAddressType =
- addressType || (await LocalCache.getItem('addressType')) || 'P2WPKH';
+ addressType ||
+ (await LocalCache.getItem('addressType')) ||
+ 'segwit-native';
dbg(
'Using address type:',
currentAddressType,
@@ -471,17 +482,17 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => {
return;
}
- const ks = JSON.parse(jks);
- // Get current address type for derivation path
- const deriveAddressType =
- (await LocalCache.getItem('addressType')) || 'legacy';
- // Check if this is a legacy wallet (created before migration timestamp)
- const useLegacyPath = isLegacyWallet(ks.created_at);
- const path = getDerivePathForNetwork(
- newNetwork,
- deriveAddressType,
- useLegacyPath,
- );
+ const ks = JSON.parse(jks);
+ // Get current address type for derivation path
+ const deriveAddressType =
+ (await LocalCache.getItem('addressType')) || 'segwit-native';
+ // Check if this is a legacy wallet (created before migration timestamp)
+ const useLegacyPath = isLegacyWallet(ks.created_at);
+ const path = getDerivePathForNetwork(
+ newNetwork,
+ deriveAddressType,
+ useLegacyPath,
+ );
btcPub = await BBMTLibNativeModule.derivePubkey(
ks.pub_key,
ks.chain_code_hex,
@@ -619,10 +630,15 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => {
const ks = JSON.parse(jks);
// Get current address type for derivation path
- const currentAddressType = (await LocalCache.getItem('addressType')) || 'legacy';
+ const currentAddressType =
+ (await LocalCache.getItem('addressType')) || 'segwit-native';
// Check if this is a legacy wallet (created before migration timestamp)
const useLegacyPath = isLegacyWallet(ks.created_at);
- const path = getDerivePathForNetwork(network, currentAddressType, useLegacyPath);
+ const path = getDerivePathForNetwork(
+ network,
+ currentAddressType,
+ useLegacyPath,
+ );
// Always derive btcPub fresh to ensure it's current
const btcPub = await BBMTLibNativeModule.derivePubkey(
@@ -641,7 +657,8 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => {
dbg('Re-initializing for network:', net);
// Get current address type
- const addrType = (await LocalCache.getItem('addressType')) || 'legacy';
+ const addrType =
+ (await LocalCache.getItem('addressType')) || 'segwit-native';
setAddressType(addrType);
// Set up network parameters
@@ -813,7 +830,7 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => {
label: string;
supportsLocal: boolean;
supportsNostr: boolean;
- type: 'basic' | 'flexi';
+ type: 'duo' | 'trio';
pubKey: string;
chainCode: string;
xpub: string;
@@ -847,9 +864,50 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => {
}
};
-
const {theme} = useTheme();
- const styles = createStyles(theme);
+ const styles = {
+ ...createStyles(theme),
+ // Lock FAB
+ lockFAB: {
+ position: 'absolute' as const,
+ bottom: 24,
+ right: 20,
+ width: 56,
+ height: 56,
+ borderRadius: 28,
+ backgroundColor:
+ Platform.OS === 'android'
+ ? 'rgba(40, 40, 50, 0.92)' // More visible dark blue-gray background on Android
+ : 'rgba(0, 0, 0, 0.6)',
+ justifyContent: 'center' as const,
+ alignItems: 'center' as const,
+ elevation: 12,
+ shadowColor: '#000',
+ shadowOffset: {width: 0, height: 4},
+ shadowOpacity: 0.5,
+ shadowRadius: 10,
+ borderWidth: Platform.OS === 'android' ? 2 : 1,
+ borderColor:
+ Platform.OS === 'android'
+ ? 'rgba(255, 255, 255, 0.35)' // More visible border on Android
+ : 'rgba(255, 255, 255, 0.2)',
+ overflow: 'hidden' as const,
+ } as const,
+ lockFABIcon: {
+ width: 28,
+ height: 28,
+ tintColor: theme.colors.background,
+ } as const,
+ lockFABOverlay: {
+ position: 'absolute' as const,
+ width: '100%',
+ height: '100%',
+ borderRadius: 28,
+ backgroundColor: 'rgba(255, 255, 255, 0.08)',
+ borderWidth: 1,
+ borderColor: 'rgba(255, 255, 255, 0.15)',
+ } as const,
+ };
const headerRight = React.useCallback(
() => ,
@@ -909,11 +967,15 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => {
useEffect(() => {
LocalCache.getItem('addressType').then(addrType => {
- setAddressType(addrType || 'legacy');
+ setAddressType(addrType || 'segwit-native');
});
LocalCache.getItem('currency').then(currency => {
setSelectedCurrency(currency || 'USD');
});
+ // Load balance visibility preference
+ LocalCache.getItem('balanceHidden').then(hidden => {
+ setIsBlurred(hidden === 'true');
+ });
});
// Simplified focus effect - just refresh data when screen comes into focus
@@ -1063,10 +1125,15 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => {
}
// Get current address type for derivation path
- const currentAddressType = (await LocalCache.getItem('addressType')) || 'legacy';
+ const currentAddressType =
+ (await LocalCache.getItem('addressType')) || 'segwit-native';
// Check if this is a legacy wallet (created before migration timestamp)
const useLegacyPath = isLegacyWallet(ks.created_at);
- const path = getDerivePathForNetwork(network, currentAddressType, useLegacyPath);
+ const path = getDerivePathForNetwork(
+ network,
+ currentAddressType,
+ useLegacyPath,
+ );
const btcPub = await BBMTLibNativeModule.derivePubkey(
ks.pub_key,
ks.chain_code_hex,
@@ -1088,9 +1155,9 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => {
// Set default address type if not set
let addrType = await LocalCache.getItem('addressType');
if (!addrType) {
- addrType = 'legacy';
+ addrType = 'segwit-native';
await LocalCache.setItem('addressType', addrType);
- dbg('WalletHome: Setting default address type to legacy');
+ dbg('WalletHome: Setting default address type to segwit-native');
}
// Set default currency if not set
let currency = (await LocalCache.getItem('currency')) || 'USD';
@@ -1243,7 +1310,7 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => {
const handleBlurred = () => {
const blurr = !isBlurred;
setIsBlurred(blurr);
- LocalCache.setItem('mode', blurr ? 'private' : '');
+ LocalCache.setItem('balanceHidden', blurr ? 'true' : 'false');
};
const loadKeyshareInfo = useCallback(async () => {
@@ -1261,9 +1328,9 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => {
const supportsNostr = !!(nostrNpub && nostrNpub.trim() !== '');
const supportsLocal = true; // Always supported
- // Determine type: basic (2 devices) or flexi (3 devices)
+ // Determine type: duo (2 devices) or trio (3 devices)
const committeeKeys = keyshare.keygen_committee_keys || [];
- const type = committeeKeys.length === 3 ? 'flexi' : 'basic';
+ const type = committeeKeys.length === 3 ? 'trio' : 'duo';
// Determine label: if Nostr, use sorted order; otherwise use generic
let label = 'KeyShare1';
@@ -1292,7 +1359,7 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => {
chainCode,
network,
keyshare.created_at,
- addressType || 'legacy',
+ addressType || 'segwit-native',
);
const outputDescriptors = {
@@ -1415,8 +1482,74 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => {
}),
);
setPendingSendParams(null);
+ setScannedFromQR(false); // Reset flag
};
+ // Process scanned QR data
+ const processScannedQRData = useCallback((qrData: string) => {
+ dbg('Scanned QR data:', qrData.substring(0, 100));
+
+ const decoded = decodeSendBitcoinQR(qrData) as {
+ toAddress: string;
+ amountSats: string;
+ feeSats: string;
+ spendingHash?: string;
+ } | null;
+ if (
+ !decoded ||
+ !decoded.toAddress ||
+ !decoded.amountSats ||
+ !decoded.feeSats
+ ) {
+ Alert.alert(
+ 'Invalid QR Code',
+ 'The scanned QR code does not contain valid send bitcoin data. Please scan the QR code from the device that initiated the transaction.',
+ );
+ return;
+ }
+
+ // Validate Bitcoin address
+ if (!validateBitcoinAddress(decoded.toAddress)) {
+ Alert.alert(
+ 'Invalid Address',
+ 'The scanned QR code contains an invalid Bitcoin address.',
+ );
+ return;
+ }
+
+ // Convert to Big for consistency
+ const amountSats = Big(decoded.amountSats);
+ const feeSats = Big(decoded.feeSats);
+
+ if (amountSats.lte(0) || feeSats.lte(0)) {
+ Alert.alert(
+ 'Invalid Amount',
+ 'The scanned QR code contains invalid amount or fee values.',
+ );
+ return;
+ }
+
+ // Store params and mark as scanned from QR
+ setPendingSendParams({
+ to: decoded.toAddress,
+ amountSats,
+ feeSats,
+ spendingHash: decoded.spendingHash || '',
+ });
+ setScannedFromQR(true);
+
+ // Show transport selector immediately (no QR code shown since data came from scan)
+ setTimeout(() => {
+ setIsTransportModalVisible(true);
+ }, 300);
+ }, []);
+
+ // Handle QR scan for send bitcoin data
+ const handleScanQRForSend = useCallback(() => {
+ HapticFeedback.medium();
+ setIsQRScannerVisible(true);
+ }, []);
+
// Handle PSBT signing - similar to handleSend (kept for compatibility with PSBT flows)
const handlePSBTSign = async (psbtBase64: string, derivePath?: string) => {
setIsPSBTModalVisible(false);
@@ -1425,13 +1558,19 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => {
const jks = await EncryptedStorage.getItem('keyshare');
if (jks) {
const ks = JSON.parse(jks);
- const currentAddressType = (await LocalCache.getItem('addressType')) || 'legacy';
+ const currentAddressType =
+ (await LocalCache.getItem('addressType')) || 'segwit-native';
// Check if this is a legacy wallet (created before migration timestamp)
const useLegacyPath = isLegacyWallet(ks.created_at);
- derivePath = getDerivePathForNetwork(network, currentAddressType, useLegacyPath);
+ derivePath = getDerivePathForNetwork(
+ network,
+ currentAddressType,
+ useLegacyPath,
+ );
}
}
- const psbtDerivePath = derivePath || getDerivePathForNetwork(network, 'legacy', true);
+ const psbtDerivePath =
+ derivePath || getDerivePathForNetwork(network, 'segwit-native', true);
// Check if keyshare supports Nostr (has nostr_npub)
try {
@@ -1463,7 +1602,7 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => {
}
// Store params and show transport selector
- setPendingPSBTParams({psbtBase64, derivePath: psbtDerivePath});
+ setPendingPSBTParams({psbtBase64});
setTimeout(() => {
setIsPSBTTransportModalVisible(true);
}, 300);
@@ -1472,7 +1611,7 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => {
const navigateToPSBTSigning = (transport: 'local' | 'nostr') => {
if (!pendingPSBTParams) return;
- const {psbtBase64, derivePath} = pendingPSBTParams;
+ const {psbtBase64} = pendingPSBTParams;
const routeName =
transport === 'local' ? 'Devices Pairing' : 'Nostr Connect';
@@ -1483,7 +1622,6 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => {
mode: 'sign_psbt',
addressType,
psbtBase64,
- derivePath,
},
}),
);
@@ -1508,7 +1646,7 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => {
}
return (
-
+
@@ -1687,16 +1825,13 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => {
/>
Send
- {/* Lock icon button replaces address type change button */}
+ {/* Scan QR button replaces lock button in action row */}
{
- HapticFeedback.light();
- // Emit a reload event to App.tsx to trigger authentication lock
- DeviceEventEmitter.emit('app:reload');
- }}>
+ onPress={handleScanQRForSend}
+ activeOpacity={0.8}>
@@ -1774,6 +1909,37 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => {
isBlurred={isBlurred}
/>
+ {/* Lock FAB Button - Bottom Right */}
+ {isInitialized && address && (
+ {
+ HapticFeedback.light();
+ // Emit a reload event to App.tsx to trigger authentication lock
+ DeviceEventEmitter.emit('app:reload');
+ }}
+ activeOpacity={0.7}>
+ {Platform.OS === 'android' && }
+
+
+ )}
+ {/* Scan QR Button - Hidden, accessible via SendBitcoinModal or other means */}
+ {/* QR Scanner Modal */}
+ setIsQRScannerVisible(false)}
+ onScan={(data: string) => {
+ setIsQRScannerVisible(false);
+ processScannedQRData(data);
+ }}
+ mode="single"
+ title="Scan Send Bitcoin QR"
+ subtitle="Point camera at the QR data on the sending device"
+ />
= ({navigation}) => {
HapticFeedback.medium();
setIsTransportModalVisible(false);
setPendingSendParams(null);
+ setScannedFromQR(false);
}}
onSelect={(transport: 'local' | 'nostr') => {
navigateToPairing(transport);
setIsTransportModalVisible(false);
}}
title="Select Signing Method"
- description="Choose how to sign your transaction"
+ description=""
+ sendBitcoinData={
+ pendingSendParams
+ ? {
+ toAddress: pendingSendParams.to,
+ amountSats: pendingSendParams.amountSats
+ .toString()
+ .split('.')[0],
+ feeSats: pendingSendParams.feeSats.toString().split('.')[0],
+ spendingHash: pendingSendParams.spendingHash,
+ }
+ : null
+ }
+ showQRCode={!scannedFromQR} // Don't show QR if data came from scan
/>
{isReceiveModalVisible && (
@@ -2005,7 +2185,6 @@ const WalletHome: React.FC<{navigation: any}> = ({navigation}) => {
nonDismissible={true}
/>
-
{
diff --git a/screens/WalletSettings.tsx b/screens/WalletSettings.tsx
index 7bc6e42..5efc259 100644
--- a/screens/WalletSettings.tsx
+++ b/screens/WalletSettings.tsx
@@ -1168,28 +1168,44 @@ const WalletSettings: React.FC<{navigation: any}> = ({navigation}) => {
}
try {
- // Haptic feedback
- HapticFeedback.light();
+ HapticFeedback.medium();
- const keyshare = await EncryptedStorage.getItem('keyshare');
- if (keyshare) {
- const json = JSON.parse(keyshare);
+ const storedKeyshare = await EncryptedStorage.getItem('keyshare');
+ if (storedKeyshare) {
+ const json = JSON.parse(storedKeyshare);
const encryptedKeyshare = await BBMTLibNativeModule.aesEncrypt(
- keyshare,
+ storedKeyshare,
await BBMTLibNativeModule.sha256(password),
);
- // Create friendly filename with date and time
- const now = new Date();
- const month = now.toLocaleDateString('en-US', {month: 'short'});
- const day = now.getDate().toString().padStart(2, '0');
- const year = now.getFullYear();
- const hours = now.getHours().toString().padStart(2, '0');
- const minutes = now.getMinutes().toString().padStart(2, '0');
- // Use keyshare label (KeyShare1/2/3) or fallback to local_party_key
+ // Create filename based on pub_key hash and keyshare number
+ if (!json.pub_key) {
+ Alert.alert('Error', 'Keyshare missing pub_key.');
+ return;
+ }
+
+ // Get SHA256 hash of pub_key and take first 4 characters
+ const pubKeyHash = await BBMTLibNativeModule.sha256(json.pub_key);
+ const hashPrefix = pubKeyHash.substring(0, 4).toLowerCase();
+
+ // Extract keyshare number from label (KeyShare1 -> 1, KeyShare2 -> 2, etc.)
const keyshareLabel = getKeyshareLabel(json);
- const shareName = keyshareLabel || json.local_party_key || 'keyshare';
- const friendlyFilename = `${shareName}.${month}${day}.${year}.${hours}${minutes}.share`;
+ let keyshareNumber = '1'; // default
+ if (keyshareLabel) {
+ const match = keyshareLabel.match(/KeyShare(\d+)/);
+ if (match) {
+ keyshareNumber = match[1];
+ }
+ } else if (json.keygen_committee_keys && json.local_party_key) {
+ // Fallback: compute from position in sorted keygen_committee_keys
+ const sortedKeys = [...json.keygen_committee_keys].sort();
+ const index = sortedKeys.indexOf(json.local_party_key);
+ if (index >= 0) {
+ keyshareNumber = String(index + 1);
+ }
+ }
+
+ const friendlyFilename = `${hashPrefix}K${keyshareNumber}.share`;
const tempDir = RNFS.TemporaryDirectoryPath || RNFS.CachesDirectoryPath;
const filePath = `${tempDir}/${friendlyFilename}`;
@@ -1213,11 +1229,12 @@ const WalletSettings: React.FC<{navigation: any}> = ({navigation}) => {
} catch {
// ignore cleanup errors
}
+ clearBackupModal();
} else {
Alert.alert('Error', 'Invalid keyshare.');
}
} catch (error) {
- dbg('backup error', error);
+ dbg('Error encrypting or sharing keyshare:', error);
Alert.alert('Error', 'Failed to encrypt or share the keyshare.');
}
};
diff --git a/utils.js b/utils.js
index c4f2a0b..7e7944a 100644
--- a/utils.js
+++ b/utils.js
@@ -539,3 +539,56 @@ export const getKeyshareLabel = keyshare => {
// Fallback: return empty string
return '';
};
+
+/**
+ * Encode send bitcoin data into QR code format
+ * Format: |||
+ * @param {string} toAddress - Bitcoin address to send to
+ * @param {string|number} amountSats - Amount in satoshis
+ * @param {string|number} feeSats - Fee in satoshis
+ * @param {string} spendingHash - Spending hash (can be empty)
+ * @returns {string} - Encoded QR data string
+ */
+export const encodeSendBitcoinQR = (toAddress, amountSats, feeSats, spendingHash = '') => {
+ const amount = typeof amountSats === 'string' ? amountSats : amountSats.toString();
+ const fee = typeof feeSats === 'string' ? feeSats : feeSats.toString();
+ return `${toAddress}|${amount}|${fee}|${spendingHash || ''}`;
+};
+
+/**
+ * Decode send bitcoin data from QR code format
+ * Format: |||
+ * @param {string} qrData - QR code data string
+ * @returns {Object|null} - Decoded data object or null if invalid
+ */
+export const decodeSendBitcoinQR = (qrData) => {
+ if (!qrData || typeof qrData !== 'string') {
+ return null;
+ }
+
+ const parts = qrData.split('|');
+ if (parts.length < 3 || parts.length > 4) {
+ return null;
+ }
+
+ const [toAddress, amountSats, feeSats, spendingHash = ''] = parts;
+
+ // Validate address is not empty
+ if (!toAddress || toAddress.trim() === '') {
+ return null;
+ }
+
+ // Validate amounts are valid numbers
+ const amount = parseInt(amountSats, 10);
+ const fee = parseInt(feeSats, 10);
+ if (isNaN(amount) || isNaN(fee) || amount < 0 || fee < 0) {
+ return null;
+ }
+
+ return {
+ toAddress: toAddress.trim(),
+ amountSats: amount.toString(),
+ feeSats: fee.toString(),
+ spendingHash: spendingHash || '',
+ };
+};