Merge pull request #7339 from BlueWallet/watfchcon

REF: Foundation for watchOS app
This commit is contained in:
GLaDOS 2024-11-30 13:38:56 +00:00 committed by GitHub
commit 96f599de6f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 1729 additions and 832 deletions

View file

@ -1,7 +1,6 @@
import { useCallback, useEffect, useRef } from 'react'; import { useCallback, useEffect, useRef } from 'react';
import { import {
transferCurrentComplicationUserInfo, transferCurrentComplicationUserInfo,
transferUserInfo,
updateApplicationContext, updateApplicationContext,
useInstalled, useInstalled,
usePaired, usePaired,
@ -9,12 +8,13 @@ import {
watchEvents, watchEvents,
} from 'react-native-watch-connectivity'; } from 'react-native-watch-connectivity';
import { MultisigHDWallet } from '../class'; import { MultisigHDWallet } from '../class';
import loc, { formatBalance, transactionTimeToReadable } from '../loc'; import loc from '../loc';
import { Chain } from '../models/bitcoinUnits'; import { Chain } from '../models/bitcoinUnits';
import { FiatUnit } from '../models/fiatUnit'; import { FiatUnit } from '../models/fiatUnit';
import { useSettings } from '../hooks/context/useSettings'; import { useSettings } from '../hooks/context/useSettings';
import { useStorage } from '../hooks/context/useStorage'; import { useStorage } from '../hooks/context/useStorage';
import { isNotificationsEnabled, majorTomToGroundControl } from '../blue_modules/notifications'; import { isNotificationsEnabled, majorTomToGroundControl } from '../blue_modules/notifications';
import { LightningTransaction, Transaction } from '../class/wallets/types';
interface Message { interface Message {
request?: string; request?: string;
@ -35,118 +35,49 @@ interface LightningInvoiceCreateRequest {
description?: string; description?: string;
} }
interface Transaction {
type: string;
amount: string;
memo: string;
time: string;
}
export function useWatchConnectivity() { export function useWatchConnectivity() {
const { walletsInitialized, wallets, fetchWalletTransactions, saveToDisk, txMetadata } = useStorage(); const { walletsInitialized, wallets, fetchWalletTransactions, saveToDisk, txMetadata } = useStorage();
const { preferredFiatCurrency } = useSettings(); const { preferredFiatCurrency } = useSettings();
const isReachable = useReachability(); const isReachable = useReachability();
const isInstalled = useInstalled(); const isInstalled = useInstalled();
const isPaired = usePaired(); const isPaired = usePaired();
const messagesListenerActive = useRef(false); const messagesListenerActive = useRef(false);
const lastPreferredCurrency = useRef(FiatUnit.USD.endPointKey); const lastPreferredCurrency = useRef(FiatUnit.USD.endPointKey);
// Set up message listener only when conditions are met const createContextPayload = () => ({
useEffect(() => { randomID: `${Date.now()}${Math.floor(Math.random() * 1000)}`,
if (!isInstalled || !isPaired || !walletsInitialized || !isReachable) { });
console.debug('Apple Watch not installed, not paired, or other conditions not met. Exiting message listener setup.');
return;
}
const messagesListener = watchEvents.addListener('message', (message: any) => handleMessages(message, () => {}));
messagesListenerActive.current = true;
return () => {
messagesListener();
messagesListenerActive.current = false;
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [walletsInitialized, isReachable, isInstalled, isPaired]);
// Send wallet data to Apple Watch
useEffect(() => {
if (!isInstalled || !isPaired || !walletsInitialized) return;
const sendWalletData = async () => {
try {
const walletsToProcess = await constructWalletsToSendToWatch();
if (walletsToProcess) {
if (isReachable) {
transferUserInfo(walletsToProcess);
console.debug('Apple Watch: sent info to watch transferUserInfo');
} else {
updateApplicationContext(walletsToProcess);
console.debug('Apple Watch: sent info to watch context');
}
}
} catch (error) {
console.debug('Failed to send wallets to watch:', error);
}
};
sendWalletData();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [walletsInitialized, isReachable, isInstalled, isPaired]);
// Update application context with wallet status
useEffect(() => { useEffect(() => {
if (!isInstalled || !isPaired || !walletsInitialized || !isReachable) return; if (!isInstalled || !isPaired || !walletsInitialized || !isReachable) return;
updateApplicationContext({ isWalletsInitialized: walletsInitialized, randomID: Math.floor(Math.random() * 11) }); const contextPayload = createContextPayload();
try {
updateApplicationContext(contextPayload);
console.debug('Transferred user info:', contextPayload);
} catch (error) {
console.error('Failed to transfer user info:', error);
}
}, [isReachable, walletsInitialized, isInstalled, isPaired]); }, [isReachable, walletsInitialized, isInstalled, isPaired]);
// Update preferred fiat currency to Apple Watch if it changes
useEffect(() => { useEffect(() => {
if (!isInstalled || !isPaired || !walletsInitialized || !isReachable || !preferredFiatCurrency) return; if (!isInstalled || !isPaired || !walletsInitialized || !isReachable || !preferredFiatCurrency) return;
if (lastPreferredCurrency.current !== preferredFiatCurrency.endPointKey) { if (lastPreferredCurrency.current !== preferredFiatCurrency.endPointKey) {
try { try {
transferCurrentComplicationUserInfo({ preferredFiatCurrency: preferredFiatCurrency.endPointKey }); const currencyPayload = { preferredFiatCurrency: preferredFiatCurrency.endPointKey };
transferCurrentComplicationUserInfo(currencyPayload);
lastPreferredCurrency.current = preferredFiatCurrency.endPointKey; lastPreferredCurrency.current = preferredFiatCurrency.endPointKey;
console.debug('Apple Watch: updated preferred fiat currency'); console.debug('Apple Watch: updated preferred fiat currency', currencyPayload);
} catch (error) { } catch (error) {
console.debug('Error updating preferredFiatCurrency on watch:', error); console.error('Error updating preferredFiatCurrency on watch:', error);
} }
} else { } else {
console.debug('WatchConnectivity lastPreferredCurrency has not changed'); console.debug('WatchConnectivity: preferred currency has not changed');
} }
}, [preferredFiatCurrency, walletsInitialized, isReachable, isInstalled, isPaired]); }, [preferredFiatCurrency, walletsInitialized, isReachable, isInstalled, isPaired]);
const handleMessages = useCallback(
async (message: Message, reply: Reply) => {
try {
if (message.request === 'createInvoice') {
const createInvoiceRequest = await handleLightningInvoiceCreateRequest({
walletIndex: message.walletIndex!,
amount: message.amount!,
description: message.description,
});
reply({ invoicePaymentRequest: createInvoiceRequest });
} else if (message.message === 'sendApplicationContext') {
const walletsToProcess = await constructWalletsToSendToWatch();
if (walletsToProcess) updateApplicationContext(walletsToProcess);
} else if (message.message === 'fetchTransactions') {
await fetchWalletTransactions();
await saveToDisk();
reply({});
} else if (message.message === 'hideBalance') {
wallets[message.walletIndex!].hideBalance = message.hideBalance!;
await saveToDisk();
reply({});
}
} catch (error) {
console.debug('Error handling message:', error);
reply({});
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[fetchWalletTransactions, saveToDisk, wallets],
);
const handleLightningInvoiceCreateRequest = useCallback( const handleLightningInvoiceCreateRequest = useCallback(
async ({ walletIndex, amount, description = loc.lnd.placeholder }: LightningInvoiceCreateRequest): Promise<string | undefined> => { async ({ walletIndex, amount, description = loc.lnd.placeholder }: LightningInvoiceCreateRequest): Promise<string | undefined> => {
const wallet = wallets[walletIndex]; const wallet = wallets[walletIndex];
@ -159,53 +90,46 @@ export function useWatchConnectivity() {
majorTomToGroundControl([], [decoded.payment_hash], []); majorTomToGroundControl([], [decoded.payment_hash], []);
return invoiceRequest; return invoiceRequest;
} }
console.debug('Created Lightning invoice:', { invoiceRequest });
return invoiceRequest; return invoiceRequest;
} }
} catch (invoiceError) { } catch (invoiceError) {
console.debug('Error creating invoice:', invoiceError); console.error('Error creating invoice:', invoiceError);
} }
} }
}, },
[wallets], [wallets],
); );
// Construct wallet data to send to the watch, including transaction details
const constructWalletsToSendToWatch = useCallback(async () => { const constructWalletsToSendToWatch = useCallback(async () => {
if (!Array.isArray(wallets) || !walletsInitialized) return; if (!Array.isArray(wallets) || !walletsInitialized) return;
const walletsToProcess = await Promise.allSettled( const walletsToProcess = await Promise.allSettled(
wallets.map(async wallet => { wallets.map(async wallet => {
try { try {
let receiveAddress; const receiveAddress = wallet.chain === Chain.ONCHAIN ? await wallet.getAddressAsync() : wallet.getAddress();
try {
receiveAddress = wallet.chain === Chain.ONCHAIN ? await wallet.getAddressAsync() : wallet.getAddress();
} catch {
receiveAddress =
wallet.chain === Chain.ONCHAIN
? 'next_free_address_index' in wallet && '_getExternalAddressByIndex' in wallet
? wallet._getExternalAddressByIndex(wallet.next_free_address_index)
: wallet.getAddress()
: wallet.getAddress();
}
const transactions: Transaction[] = wallet const transactions: Transaction[] = wallet
.getTransactions() .getTransactions()
.slice(0, 10) .slice(0, 10)
.map((transaction: any) => ({ .map((transaction: Transaction & LightningTransaction) => ({
type: transaction.confirmations ? 'pendingConfirmation' : 'received', type: determineTransactionType(transaction),
amount: formatBalance(transaction.value, wallet.getPreferredBalanceUnit(), true).toString(), amount: transaction.value ?? 0,
memo: txMetadata[transaction.hash]?.memo || transaction.memo || '', memo:
time: transactionTimeToReadable(transaction.received), 'hash' in (transaction as Transaction)
? txMetadata[(transaction as Transaction).hash]?.memo || transaction.memo || ''
: transaction.memo || '',
time: transaction.received ?? transaction.time,
})); }));
return { const walletData = {
label: wallet.getLabel(), label: wallet.getLabel(),
balance: formatBalance(Number(wallet.getBalance()), wallet.getPreferredBalanceUnit(), true), balance: Number(wallet.getBalance()),
type: wallet.type, type: wallet.type,
preferredBalanceUnit: wallet.getPreferredBalanceUnit(), preferredBalanceUnit: wallet.getPreferredBalanceUnit(),
receiveAddress, receiveAddress,
transactions, transactions,
hideBalance: wallet.hideBalance, chain: wallet.chain,
hideBalance: wallet.hideBalance ? 1 : 0,
...(wallet.chain === Chain.ONCHAIN && ...(wallet.chain === Chain.ONCHAIN &&
wallet.type !== MultisigHDWallet.type && { wallet.type !== MultisigHDWallet.type && {
xpub: wallet.getXpub() || wallet.getSecret(), xpub: wallet.getXpub() || wallet.getSecret(),
@ -214,13 +138,17 @@ export function useWatchConnectivity() {
wallet.isBIP47Enabled() && wallet.isBIP47Enabled() &&
'getBIP47PaymentCode' in wallet && { paymentCode: wallet.getBIP47PaymentCode() }), 'getBIP47PaymentCode' in wallet && { paymentCode: wallet.getBIP47PaymentCode() }),
}; };
} catch (error) {
console.error('Failed to construct wallet:', { console.debug('Constructed wallet data for watch:', {
walletLabel: wallet.getLabel(), label: walletData.label,
walletType: wallet.type, type: walletData.type,
error, preferredBalanceUnit: walletData.preferredBalanceUnit,
transactionCount: transactions.length,
}); });
return null; // Ensure failed wallet returns null so it's excluded from final results return walletData;
} catch (error) {
console.error('Failed to construct wallet data:', error);
return null;
} }
}), }),
); );
@ -229,10 +157,124 @@ export function useWatchConnectivity() {
.filter(result => result.status === 'fulfilled' && result.value !== null) .filter(result => result.status === 'fulfilled' && result.value !== null)
.map(result => (result as PromiseFulfilledResult<any>).value); .map(result => (result as PromiseFulfilledResult<any>).value);
console.debug('Constructed wallets to process for Apple Watch'); console.debug('Constructed wallets to process for Apple Watch:', {
return { wallets: processedWallets, randomID: Math.floor(Math.random() * 11) }; walletCount: processedWallets.length,
walletLabels: processedWallets.map(wallet => wallet.label),
});
return { wallets: processedWallets, randomID: `${Date.now()}${Math.floor(Math.random() * 1000)}` };
}, [wallets, walletsInitialized, txMetadata]); }, [wallets, walletsInitialized, txMetadata]);
const determineTransactionType = (transaction: Transaction & LightningTransaction): string => {
const confirmations = (transaction as Transaction).confirmations ?? 0;
if (confirmations < 3) {
return 'pending_transaction';
}
if (transaction.type === 'bitcoind_tx') {
return 'onchain';
}
if (transaction.type === 'paid_invoice') {
return 'offchain';
}
if (transaction.type === 'user_invoice' || transaction.type === 'payment_request') {
const currentDate = new Date();
const now = Math.floor(currentDate.getTime() / 1000);
const timestamp = transaction.timestamp ?? 0;
const expireTime = transaction.expire_time ?? 0;
const invoiceExpiration = timestamp + expireTime;
if (!transaction.ispaid && invoiceExpiration < now) {
return 'expired_transaction';
} else {
return 'incoming_transaction';
}
}
if ((transaction.value ?? 0) < 0) {
return 'outgoing_transaction';
} else {
return 'incoming_transaction';
}
};
const handleMessages = useCallback(
async (message: Message, reply: Reply) => {
console.debug('Received message from Apple Watch:', message);
try {
if (message.request === 'createInvoice' && typeof message.walletIndex === 'number' && typeof message.amount === 'number') {
const createInvoiceRequest = await handleLightningInvoiceCreateRequest({
walletIndex: message.walletIndex,
amount: message.amount,
description: message.description,
});
reply({ invoicePaymentRequest: createInvoiceRequest });
} else if (message.message === 'sendApplicationContext') {
const walletsToProcess = await constructWalletsToSendToWatch();
if (walletsToProcess) {
updateApplicationContext(walletsToProcess);
console.debug('Transferred user info on request:', walletsToProcess);
}
} else if (message.message === 'fetchTransactions') {
await fetchWalletTransactions();
await saveToDisk();
reply({});
} else if (
message.message === 'hideBalance' &&
typeof message.walletIndex === 'number' &&
typeof message.hideBalance === 'boolean' &&
message.walletIndex >= 0 &&
message.walletIndex < wallets.length
) {
wallets[message.walletIndex].hideBalance = message.hideBalance;
await saveToDisk();
reply({});
}
} catch (error) {
console.error('Error handling message:', error);
reply({});
}
},
[fetchWalletTransactions, saveToDisk, wallets, constructWalletsToSendToWatch, handleLightningInvoiceCreateRequest],
);
useEffect(() => {
if (!isInstalled || !isPaired || !walletsInitialized) return;
const sendWalletData = async () => {
try {
const walletsToProcess = await constructWalletsToSendToWatch();
if (walletsToProcess) {
updateApplicationContext(walletsToProcess);
console.debug('Apple Watch: sent wallet data via transferUserInfo', walletsToProcess);
}
} catch (error) {
console.error('Failed to send wallets to watch:', error);
}
};
sendWalletData();
}, [walletsInitialized, isInstalled, isPaired, constructWalletsToSendToWatch]);
useEffect(() => {
if (!isInstalled) return;
const unsubscribe = watchEvents.addListener('message', (message: any) => {
if (message.request === 'wakeUpApp') {
console.debug('Received wake-up request from Apple Watch');
} else {
handleMessages(message, () => {});
}
});
messagesListenerActive.current = true;
console.debug('Message listener set up for Apple Watch');
return () => {
unsubscribe();
messagesListenerActive.current = false;
console.debug('Message listener for Apple Watch cleaned up');
};
}, [isInstalled, handleMessages]);
} }
export default useWatchConnectivity; export default useWatchConnectivity;

View file

@ -125,6 +125,14 @@
B4742E992CCDBE8300380EEE /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = B4742E962CCDBE8300380EEE /* Localizable.xcstrings */; }; B4742E992CCDBE8300380EEE /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = B4742E962CCDBE8300380EEE /* Localizable.xcstrings */; };
B4742E9A2CCDBE8300380EEE /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = B4742E962CCDBE8300380EEE /* Localizable.xcstrings */; }; B4742E9A2CCDBE8300380EEE /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = B4742E962CCDBE8300380EEE /* Localizable.xcstrings */; };
B4742E9B2CCDBE8300380EEE /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = B4742E962CCDBE8300380EEE /* Localizable.xcstrings */; }; B4742E9B2CCDBE8300380EEE /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = B4742E962CCDBE8300380EEE /* Localizable.xcstrings */; };
B4793DBB2CEDACBD00C92C2E /* Chain.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4793DBA2CEDACBD00C92C2E /* Chain.swift */; };
B4793DBC2CEDACBD00C92C2E /* Chain.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4793DBA2CEDACBD00C92C2E /* Chain.swift */; };
B4793DBD2CEDACBD00C92C2E /* Chain.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4793DBA2CEDACBD00C92C2E /* Chain.swift */; };
B4793DBF2CEDACDA00C92C2E /* TransactionType.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4793DBE2CEDACDA00C92C2E /* TransactionType.swift */; };
B4793DC12CEDACE700C92C2E /* WalletType.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4793DC02CEDACE700C92C2E /* WalletType.swift */; };
B4793DC32CEDAD4400C92C2E /* KeychainHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4793DC22CEDAD4400C92C2E /* KeychainHelper.swift */; };
B4793DC42CEDAD4400C92C2E /* KeychainHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4793DC22CEDAD4400C92C2E /* KeychainHelper.swift */; };
B4793DC52CEDAD4400C92C2E /* KeychainHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4793DC22CEDAD4400C92C2E /* KeychainHelper.swift */; };
B48630D62CCEE67100A8425C /* PriceWidgetProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = B48630D52CCEE67100A8425C /* PriceWidgetProvider.swift */; }; B48630D62CCEE67100A8425C /* PriceWidgetProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = B48630D52CCEE67100A8425C /* PriceWidgetProvider.swift */; };
B48630DD2CCEE7AC00A8425C /* PriceWidgetEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = B48630DC2CCEE7AC00A8425C /* PriceWidgetEntry.swift */; }; B48630DD2CCEE7AC00A8425C /* PriceWidgetEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = B48630DC2CCEE7AC00A8425C /* PriceWidgetEntry.swift */; };
B48630DE2CCEE7AC00A8425C /* PriceWidgetEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = B48630DC2CCEE7AC00A8425C /* PriceWidgetEntry.swift */; }; B48630DE2CCEE7AC00A8425C /* PriceWidgetEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = B48630DC2CCEE7AC00A8425C /* PriceWidgetEntry.swift */; };
@ -137,7 +145,6 @@
B48630EC2CCEEEA700A8425C /* WalletAppShortcuts.swift in Sources */ = {isa = PBXBuildFile; fileRef = B48630EB2CCEEEA700A8425C /* WalletAppShortcuts.swift */; }; B48630EC2CCEEEA700A8425C /* WalletAppShortcuts.swift in Sources */ = {isa = PBXBuildFile; fileRef = B48630EB2CCEEEA700A8425C /* WalletAppShortcuts.swift */; };
B48630ED2CCEEEB000A8425C /* WalletAppShortcuts.swift in Sources */ = {isa = PBXBuildFile; fileRef = B48630EB2CCEEEA700A8425C /* WalletAppShortcuts.swift */; }; B48630ED2CCEEEB000A8425C /* WalletAppShortcuts.swift in Sources */ = {isa = PBXBuildFile; fileRef = B48630EB2CCEEEA700A8425C /* WalletAppShortcuts.swift */; };
B48630EE2CCEEEE900A8425C /* PriceIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = B48630D02CCEE3B300A8425C /* PriceIntent.swift */; }; B48630EE2CCEEEE900A8425C /* PriceIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = B48630D02CCEE3B300A8425C /* PriceIntent.swift */; };
B48A6A292C1DF01000030AB9 /* KeychainSwift in Frameworks */ = {isa = PBXBuildFile; productRef = B48A6A282C1DF01000030AB9 /* KeychainSwift */; };
B49A28BB2CD18999006B08E4 /* CompactPriceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B49A28BA2CD18999006B08E4 /* CompactPriceView.swift */; }; B49A28BB2CD18999006B08E4 /* CompactPriceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B49A28BA2CD18999006B08E4 /* CompactPriceView.swift */; };
B49A28BC2CD18999006B08E4 /* CompactPriceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B49A28BA2CD18999006B08E4 /* CompactPriceView.swift */; }; B49A28BC2CD18999006B08E4 /* CompactPriceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B49A28BA2CD18999006B08E4 /* CompactPriceView.swift */; };
B49A28BE2CD189B0006B08E4 /* FiatUnitEnum.swift in Sources */ = {isa = PBXBuildFile; fileRef = B49A28BD2CD189B0006B08E4 /* FiatUnitEnum.swift */; }; B49A28BE2CD189B0006B08E4 /* FiatUnitEnum.swift in Sources */ = {isa = PBXBuildFile; fileRef = B49A28BD2CD189B0006B08E4 /* FiatUnitEnum.swift */; };
@ -373,6 +380,10 @@
B461B851299599F800E431AA /* AppDelegate.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = AppDelegate.mm; path = BlueWallet/AppDelegate.mm; sourceTree = "<group>"; }; B461B851299599F800E431AA /* AppDelegate.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = AppDelegate.mm; path = BlueWallet/AppDelegate.mm; sourceTree = "<group>"; };
B4742E962CCDBE8300380EEE /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; }; B4742E962CCDBE8300380EEE /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = "<group>"; };
B4742E9C2CCDC31300380EEE /* en_US */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en_US; path = en_US.lproj/Interface.strings; sourceTree = "<group>"; }; B4742E9C2CCDC31300380EEE /* en_US */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en_US; path = en_US.lproj/Interface.strings; sourceTree = "<group>"; };
B4793DBA2CEDACBD00C92C2E /* Chain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Chain.swift; sourceTree = "<group>"; };
B4793DBE2CEDACDA00C92C2E /* TransactionType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TransactionType.swift; sourceTree = "<group>"; };
B4793DC02CEDACE700C92C2E /* WalletType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WalletType.swift; sourceTree = "<group>"; };
B4793DC22CEDAD4400C92C2E /* KeychainHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainHelper.swift; sourceTree = "<group>"; };
B47B21EB2B2128B8001F6690 /* BlueWalletUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlueWalletUITests.swift; sourceTree = "<group>"; }; B47B21EB2B2128B8001F6690 /* BlueWalletUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlueWalletUITests.swift; sourceTree = "<group>"; };
B48630D02CCEE3B300A8425C /* PriceIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PriceIntent.swift; sourceTree = "<group>"; }; B48630D02CCEE3B300A8425C /* PriceIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PriceIntent.swift; sourceTree = "<group>"; };
B48630D52CCEE67100A8425C /* PriceWidgetProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PriceWidgetProvider.swift; sourceTree = "<group>"; }; B48630D52CCEE67100A8425C /* PriceWidgetProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PriceWidgetProvider.swift; sourceTree = "<group>"; };
@ -450,7 +461,6 @@
files = ( files = (
B41B76852B66B2FF002C48D5 /* Bugsnag in Frameworks */, B41B76852B66B2FF002C48D5 /* Bugsnag in Frameworks */,
B41B76872B66B2FF002C48D5 /* BugsnagNetworkRequestPlugin in Frameworks */, B41B76872B66B2FF002C48D5 /* BugsnagNetworkRequestPlugin in Frameworks */,
B48A6A292C1DF01000030AB9 /* KeychainSwift in Frameworks */,
6DFC807024EA0B6C007B8700 /* EFQRCode in Frameworks */, 6DFC807024EA0B6C007B8700 /* EFQRCode in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
@ -733,6 +743,8 @@
B43D03242258474500FBAA95 /* Objects */ = { B43D03242258474500FBAA95 /* Objects */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
B4793DC02CEDACE700C92C2E /* WalletType.swift */,
B4793DBE2CEDACDA00C92C2E /* TransactionType.swift */,
B43D0374225847C500FBAA95 /* Transaction.swift */, B43D0374225847C500FBAA95 /* Transaction.swift */,
B43D0375225847C500FBAA95 /* TransactionTableRow.swift */, B43D0375225847C500FBAA95 /* TransactionTableRow.swift */,
B43D0376225847C500FBAA95 /* Wallet.swift */, B43D0376225847C500FBAA95 /* Wallet.swift */,
@ -750,6 +762,7 @@
B44033C82BCC34AC00162242 /* Shared */ = { B44033C82BCC34AC00162242 /* Shared */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
B4793DBA2CEDACBD00C92C2E /* Chain.swift */,
B450109A2C0FCD7E00619044 /* Utilities */, B450109A2C0FCD7E00619044 /* Utilities */,
6D2AA8062568B8E50090B089 /* Fiat */, 6D2AA8062568B8E50090B089 /* Fiat */,
6D9A2E6A254BAB1B007B5B82 /* MarketAPI.swift */, 6D9A2E6A254BAB1B007B5B82 /* MarketAPI.swift */,
@ -776,6 +789,7 @@
B450109A2C0FCD7E00619044 /* Utilities */ = { B450109A2C0FCD7E00619044 /* Utilities */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
B4793DC22CEDAD4400C92C2E /* KeychainHelper.swift */,
B450109B2C0FCD8A00619044 /* Utilities.swift */, B450109B2C0FCD8A00619044 /* Utilities.swift */,
); );
path = Utilities; path = Utilities;
@ -934,7 +948,6 @@
6DFC806F24EA0B6C007B8700 /* EFQRCode */, 6DFC806F24EA0B6C007B8700 /* EFQRCode */,
B41B76842B66B2FF002C48D5 /* Bugsnag */, B41B76842B66B2FF002C48D5 /* Bugsnag */,
B41B76862B66B2FF002C48D5 /* BugsnagNetworkRequestPlugin */, B41B76862B66B2FF002C48D5 /* BugsnagNetworkRequestPlugin */,
B48A6A282C1DF01000030AB9 /* KeychainSwift */,
); );
productName = "BlueWalletWatch Extension"; productName = "BlueWalletWatch Extension";
productReference = B40D4E3C225841ED00428FCC /* BlueWalletWatch Extension.appex */; productReference = B40D4E3C225841ED00428FCC /* BlueWalletWatch Extension.appex */;
@ -1016,7 +1029,6 @@
packageReferences = ( packageReferences = (
6DFC806E24EA0B6C007B8700 /* XCRemoteSwiftPackageReference "EFQRCode" */, 6DFC806E24EA0B6C007B8700 /* XCRemoteSwiftPackageReference "EFQRCode" */,
B41B76832B66B2FF002C48D5 /* XCRemoteSwiftPackageReference "bugsnag-cocoa" */, B41B76832B66B2FF002C48D5 /* XCRemoteSwiftPackageReference "bugsnag-cocoa" */,
B48A6A272C1DF01000030AB9 /* XCRemoteSwiftPackageReference "keychain-swift" */,
); );
productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */; productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */;
projectDirPath = ""; projectDirPath = "";
@ -1248,6 +1260,7 @@
B44034072BCC38A000162242 /* FiatUnit.swift in Sources */, B44034072BCC38A000162242 /* FiatUnit.swift in Sources */,
B44034002BCC37F800162242 /* Bundle+decode.swift in Sources */, B44034002BCC37F800162242 /* Bundle+decode.swift in Sources */,
B44033E22BCC36CB00162242 /* Placeholders.swift in Sources */, B44033E22BCC36CB00162242 /* Placeholders.swift in Sources */,
B4793DBB2CEDACBD00C92C2E /* Chain.swift in Sources */,
B4B1A4622BFA73110072E3BB /* WidgetHelper.swift in Sources */, B4B1A4622BFA73110072E3BB /* WidgetHelper.swift in Sources */,
B48630E12CCEE7C800A8425C /* PriceWidgetEntryView.swift in Sources */, B48630E12CCEE7C800A8425C /* PriceWidgetEntryView.swift in Sources */,
B44033DA2BCC369A00162242 /* Colors.swift in Sources */, B44033DA2BCC369A00162242 /* Colors.swift in Sources */,
@ -1256,6 +1269,7 @@
B49A28BC2CD18999006B08E4 /* CompactPriceView.swift in Sources */, B49A28BC2CD18999006B08E4 /* CompactPriceView.swift in Sources */,
B44033D82BCC369500162242 /* UserDefaultsExtension.swift in Sources */, B44033D82BCC369500162242 /* UserDefaultsExtension.swift in Sources */,
B44033E42BCC36FF00162242 /* WalletData.swift in Sources */, B44033E42BCC36FF00162242 /* WalletData.swift in Sources */,
B4793DC52CEDAD4400C92C2E /* KeychainHelper.swift in Sources */,
B44033BF2BCC32F800162242 /* BitcoinUnit.swift in Sources */, B44033BF2BCC32F800162242 /* BitcoinUnit.swift in Sources */,
B44034052BCC389200162242 /* XMLParserDelegate.swift in Sources */, B44034052BCC389200162242 /* XMLParserDelegate.swift in Sources */,
B48630DD2CCEE7AC00A8425C /* PriceWidgetEntry.swift in Sources */, B48630DD2CCEE7AC00A8425C /* PriceWidgetEntry.swift in Sources */,
@ -1270,6 +1284,7 @@
6DD410BE266CAF5C0087DE03 /* SendReceiveButtons.swift in Sources */, 6DD410BE266CAF5C0087DE03 /* SendReceiveButtons.swift in Sources */,
B48630E02CCEE7C800A8425C /* PriceWidgetEntryView.swift in Sources */, B48630E02CCEE7C800A8425C /* PriceWidgetEntryView.swift in Sources */,
6DD410B4266CAF5C0087DE03 /* MarketAPI.swift in Sources */, 6DD410B4266CAF5C0087DE03 /* MarketAPI.swift in Sources */,
B4793DC32CEDAD4400C92C2E /* KeychainHelper.swift in Sources */,
B40FC3FA29CCD1D00007EBAC /* SwiftTCPClient.swift in Sources */, B40FC3FA29CCD1D00007EBAC /* SwiftTCPClient.swift in Sources */,
B48630EC2CCEEEA700A8425C /* WalletAppShortcuts.swift in Sources */, B48630EC2CCEEEA700A8425C /* WalletAppShortcuts.swift in Sources */,
6DD410A1266CADF10087DE03 /* Widgets.swift in Sources */, 6DD410A1266CADF10087DE03 /* Widgets.swift in Sources */,
@ -1291,6 +1306,7 @@
B44033F02BCC374500162242 /* Numeric+abbreviated.swift in Sources */, B44033F02BCC374500162242 /* Numeric+abbreviated.swift in Sources */,
B44033DF2BCC36C300162242 /* LatestTransaction.swift in Sources */, B44033DF2BCC36C300162242 /* LatestTransaction.swift in Sources */,
6DD410C0266CB1460087DE03 /* MarketWidget.swift in Sources */, 6DD410C0266CB1460087DE03 /* MarketWidget.swift in Sources */,
B4793DBC2CEDACBD00C92C2E /* Chain.swift in Sources */,
B4AB225E2B02AD12001F4328 /* XMLParserDelegate.swift in Sources */, B4AB225E2B02AD12001F4328 /* XMLParserDelegate.swift in Sources */,
B44033F62BCC377F00162242 /* WidgetData.swift in Sources */, B44033F62BCC377F00162242 /* WidgetData.swift in Sources */,
6DD410BA266CAF5C0087DE03 /* FiatUnit.swift in Sources */, 6DD410BA266CAF5C0087DE03 /* FiatUnit.swift in Sources */,
@ -1329,6 +1345,7 @@
6DFC807224EA2FA9007B8700 /* ViewQRCodefaceController.swift in Sources */, 6DFC807224EA2FA9007B8700 /* ViewQRCodefaceController.swift in Sources */,
B40D4E46225841ED00428FCC /* NotificationController.swift in Sources */, B40D4E46225841ED00428FCC /* NotificationController.swift in Sources */,
B40D4E5D2258425500428FCC /* InterfaceController.swift in Sources */, B40D4E5D2258425500428FCC /* InterfaceController.swift in Sources */,
B4793DBD2CEDACBD00C92C2E /* Chain.swift in Sources */,
B44033FA2BCC379200162242 /* WidgetDataStore.swift in Sources */, B44033FA2BCC379200162242 /* WidgetDataStore.swift in Sources */,
B44033DE2BCC36C300162242 /* LatestTransaction.swift in Sources */, B44033DE2BCC36C300162242 /* LatestTransaction.swift in Sources */,
B4D0B2662C1DEB7F006B6B1B /* ReceiveInterfaceMode.swift in Sources */, B4D0B2662C1DEB7F006B6B1B /* ReceiveInterfaceMode.swift in Sources */,
@ -1338,6 +1355,7 @@
B40D4E642258425500428FCC /* WalletDetailsInterfaceController.swift in Sources */, B40D4E642258425500428FCC /* WalletDetailsInterfaceController.swift in Sources */,
B40D4E44225841ED00428FCC /* ExtensionDelegate.swift in Sources */, B40D4E44225841ED00428FCC /* ExtensionDelegate.swift in Sources */,
6D4AF16D25D21192009DD853 /* Placeholders.swift in Sources */, 6D4AF16D25D21192009DD853 /* Placeholders.swift in Sources */,
B4793DC42CEDAD4400C92C2E /* KeychainHelper.swift in Sources */,
B44033DB2BCC369B00162242 /* Colors.swift in Sources */, B44033DB2BCC369B00162242 /* Colors.swift in Sources */,
B40D4E632258425500428FCC /* ReceiveInterfaceController.swift in Sources */, B40D4E632258425500428FCC /* ReceiveInterfaceController.swift in Sources */,
B43D0378225847C500FBAA95 /* WalletGradient.swift in Sources */, B43D0378225847C500FBAA95 /* WalletGradient.swift in Sources */,
@ -1345,6 +1363,8 @@
B44033C02BCC32F800162242 /* BitcoinUnit.swift in Sources */, B44033C02BCC32F800162242 /* BitcoinUnit.swift in Sources */,
B44033E52BCC36FF00162242 /* WalletData.swift in Sources */, B44033E52BCC36FF00162242 /* WalletData.swift in Sources */,
B44033EF2BCC374500162242 /* Numeric+abbreviated.swift in Sources */, B44033EF2BCC374500162242 /* Numeric+abbreviated.swift in Sources */,
B4793DC12CEDACE700C92C2E /* WalletType.swift in Sources */,
B4793DBF2CEDACDA00C92C2E /* TransactionType.swift in Sources */,
B4D0B2622C1DEA11006B6B1B /* ReceivePageInterfaceController.swift in Sources */, B4D0B2622C1DEA11006B6B1B /* ReceivePageInterfaceController.swift in Sources */,
B44033D02BCC352F00162242 /* UserDefaultsGroup.swift in Sources */, B44033D02BCC352F00162242 /* UserDefaultsGroup.swift in Sources */,
B44033C52BCC332400162242 /* Balance.swift in Sources */, B44033C52BCC332400162242 /* Balance.swift in Sources */,
@ -1447,7 +1467,10 @@
); );
HEADER_SEARCH_PATHS = "$(inherited)"; HEADER_SEARCH_PATHS = "$(inherited)";
INFOPLIST_FILE = BlueWallet/Info.plist; INFOPLIST_FILE = BlueWallet/Info.plist;
INFOPLIST_KEY_CLKComplicationPrincipalClass = "$(PRODUCT_BUNDLE_IDENTIFIER).ComplicationController";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.finance"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.finance";
INFOPLIST_KEY_WKCompanionAppBundleIdentifier = io.bluewallet.bluewallet;
INFOPLIST_KEY_WKExtensionDelegateClassName = "$(PRODUCT_BUNDLE_IDENTIFIER).ExtensionDelegate";
IPHONEOS_DEPLOYMENT_TARGET = 13.4; IPHONEOS_DEPLOYMENT_TARGET = 13.4;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
@ -1502,7 +1525,10 @@
"ENABLE_HARDENED_RUNTIME[sdk=macosx*]" = YES; "ENABLE_HARDENED_RUNTIME[sdk=macosx*]" = YES;
HEADER_SEARCH_PATHS = "$(inherited)"; HEADER_SEARCH_PATHS = "$(inherited)";
INFOPLIST_FILE = BlueWallet/Info.plist; INFOPLIST_FILE = BlueWallet/Info.plist;
INFOPLIST_KEY_CLKComplicationPrincipalClass = "$(PRODUCT_BUNDLE_IDENTIFIER).ComplicationController";
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.finance"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.finance";
INFOPLIST_KEY_WKCompanionAppBundleIdentifier = io.bluewallet.bluewallet;
INFOPLIST_KEY_WKExtensionDelegateClassName = "$(PRODUCT_BUNDLE_IDENTIFIER).ExtensionDelegate";
IPHONEOS_DEPLOYMENT_TARGET = 13.4; IPHONEOS_DEPLOYMENT_TARGET = 13.4;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
@ -1886,6 +1912,9 @@
"DEVELOPMENT_TEAM[sdk=watchos*]" = A7W54YZ4WU; "DEVELOPMENT_TEAM[sdk=watchos*]" = A7W54YZ4WU;
GCC_C_LANGUAGE_STANDARD = gnu11; GCC_C_LANGUAGE_STANDARD = gnu11;
INFOPLIST_FILE = "BlueWalletWatch Extension/Info.plist"; INFOPLIST_FILE = "BlueWalletWatch Extension/Info.plist";
INFOPLIST_KEY_CLKComplicationPrincipalClass = "$(PRODUCT_BUNDLE_IDENTIFIER).ComplicationController";
INFOPLIST_KEY_WKCompanionAppBundleIdentifier = io.bluewallet.bluewallet;
INFOPLIST_KEY_WKExtensionDelegateClassName = "$(PRODUCT_BUNDLE_IDENTIFIER).ExtensionDelegate";
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
@ -1936,6 +1965,9 @@
"DEVELOPMENT_TEAM[sdk=watchos*]" = A7W54YZ4WU; "DEVELOPMENT_TEAM[sdk=watchos*]" = A7W54YZ4WU;
GCC_C_LANGUAGE_STANDARD = gnu11; GCC_C_LANGUAGE_STANDARD = gnu11;
INFOPLIST_FILE = "BlueWalletWatch Extension/Info.plist"; INFOPLIST_FILE = "BlueWalletWatch Extension/Info.plist";
INFOPLIST_KEY_CLKComplicationPrincipalClass = "$(PRODUCT_BUNDLE_IDENTIFIER).ComplicationController";
INFOPLIST_KEY_WKCompanionAppBundleIdentifier = io.bluewallet.bluewallet;
INFOPLIST_KEY_WKExtensionDelegateClassName = "$(PRODUCT_BUNDLE_IDENTIFIER).ExtensionDelegate";
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
@ -1986,6 +2018,7 @@
GCC_C_LANGUAGE_STANDARD = gnu11; GCC_C_LANGUAGE_STANDARD = gnu11;
IBSC_MODULE = BlueWalletWatch_Extension; IBSC_MODULE = BlueWalletWatch_Extension;
INFOPLIST_FILE = BlueWalletWatch/Info.plist; INFOPLIST_FILE = BlueWalletWatch/Info.plist;
INFOPLIST_KEY_WKCompanionAppBundleIdentifier = io.bluewallet.bluewallet;
LIBRARY_SEARCH_PATHS = ( LIBRARY_SEARCH_PATHS = (
"$(SDKROOT)/usr/lib/swift", "$(SDKROOT)/usr/lib/swift",
"$(SDKROOT)/System/iOSSupport/usr/lib/swift", "$(SDKROOT)/System/iOSSupport/usr/lib/swift",
@ -2034,6 +2067,7 @@
GCC_C_LANGUAGE_STANDARD = gnu11; GCC_C_LANGUAGE_STANDARD = gnu11;
IBSC_MODULE = BlueWalletWatch_Extension; IBSC_MODULE = BlueWalletWatch_Extension;
INFOPLIST_FILE = BlueWalletWatch/Info.plist; INFOPLIST_FILE = BlueWalletWatch/Info.plist;
INFOPLIST_KEY_WKCompanionAppBundleIdentifier = io.bluewallet.bluewallet;
LIBRARY_SEARCH_PATHS = ( LIBRARY_SEARCH_PATHS = (
"$(SDKROOT)/usr/lib/swift", "$(SDKROOT)/usr/lib/swift",
"$(SDKROOT)/System/iOSSupport/usr/lib/swift", "$(SDKROOT)/System/iOSSupport/usr/lib/swift",
@ -2133,14 +2167,6 @@
version = 6.28.1; version = 6.28.1;
}; };
}; };
B48A6A272C1DF01000030AB9 /* XCRemoteSwiftPackageReference "keychain-swift" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/evgenyneu/keychain-swift.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 24.0.0;
};
};
/* End XCRemoteSwiftPackageReference section */ /* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */ /* Begin XCSwiftPackageProductDependency section */
@ -2159,11 +2185,6 @@
package = B41B76832B66B2FF002C48D5 /* XCRemoteSwiftPackageReference "bugsnag-cocoa" */; package = B41B76832B66B2FF002C48D5 /* XCRemoteSwiftPackageReference "bugsnag-cocoa" */;
productName = BugsnagNetworkRequestPlugin; productName = BugsnagNetworkRequestPlugin;
}; };
B48A6A282C1DF01000030AB9 /* KeychainSwift */ = {
isa = XCSwiftPackageProductDependency;
package = B48A6A272C1DF01000030AB9 /* XCRemoteSwiftPackageReference "keychain-swift" */;
productName = KeychainSwift;
};
/* End XCSwiftPackageProductDependency section */ /* End XCSwiftPackageProductDependency section */
}; };
rootObject = 83CBB9F71A601CBA00E9B192 /* Project object */; rootObject = 83CBB9F71A601CBA00E9B192 /* Project object */;

View file

@ -1,5 +1,5 @@
{ {
"originHash" : "52530e6b1e3a85c8854952ef703a6d1bbe1acd82713be2b3166476b9b277db23", "originHash" : "89509f555bc90a15b96ca0a326a69850770bdaac04a46f9cf482d81533702e3c",
"pins" : [ "pins" : [
{ {
"identity" : "bugsnag-cocoa", "identity" : "bugsnag-cocoa",
@ -19,15 +19,6 @@
"version" : "6.2.2" "version" : "6.2.2"
} }
}, },
{
"identity" : "keychain-swift",
"kind" : "remoteSourceControl",
"location" : "https://github.com/evgenyneu/keychain-swift.git",
"state" : {
"revision" : "5e1b02b6a9dac2a759a1d5dbc175c86bd192a608",
"version" : "24.0.0"
}
},
{ {
"identity" : "swift_qrcodejs", "identity" : "swift_qrcodejs",
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",

View file

@ -2,351 +2,338 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>LSMinimumSystemVersion</key> <key>LSMinimumSystemVersion</key>
<string>11</string> <string>11</string>
<key>BGTaskSchedulerPermittedIdentifiers</key> <key>BGTaskSchedulerPermittedIdentifiers</key>
<array> <array>
<string>io.bluewallet.bluewallet.fetchTxsForWallet</string> <string>io.bluewallet.bluewallet.fetchTxsForWallet</string>
</array> </array>
<key>CADisableMinimumFrameDurationOnPhone</key> <key>CADisableMinimumFrameDurationOnPhone</key>
<true/> <true/>
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
<string>en</string> <string>en</string>
<key>CFBundleDisplayName</key> <key>CFBundleDisplayName</key>
<string>BlueWallet</string> <string>BlueWallet</string>
<key>CFBundleDocumentTypes</key> <key>CFBundleDocumentTypes</key>
<array> <array>
<!-- PSBT file type --> <dict>
<dict> <key>CFBundleTypeIconFiles</key>
<key>CFBundleTypeIconFiles</key> <array/>
<array/> <key>CFBundleTypeName</key>
<key>CFBundleTypeName</key> <string>PSBT</string>
<string>PSBT</string> <key>CFBundleTypeRole</key>
<key>CFBundleTypeRole</key> <string>Editor</string>
<string>Editor</string> <key>LSHandlerRank</key>
<key>LSHandlerRank</key> <string>Owner</string>
<string>Owner</string> <key>LSItemContentTypes</key>
<key>LSItemContentTypes</key> <array>
<array> <string>io.bluewallet.psbt</string>
<string>io.bluewallet.psbt</string> </array>
</array> </dict>
</dict> <dict>
<!-- Image file types --> <key>CFBundleTypeIconFiles</key>
<dict> <array/>
<key>CFBundleTypeIconFiles</key> <key>CFBundleTypeName</key>
<array/> <string>Image</string>
<key>CFBundleTypeName</key> <key>CFBundleTypeRole</key>
<string>Image</string> <string>Viewer</string>
<key>CFBundleTypeRole</key> <key>LSHandlerRank</key>
<string>Viewer</string> <string>Alternate</string>
<key>LSHandlerRank</key> <key>LSItemContentTypes</key>
<string>Alternate</string> <array>
<key>LSItemContentTypes</key> <string>public.jpeg</string>
<array> <string>public.image</string>
<string>public.jpeg</string> </array>
<string>public.image</string> </dict>
</array> <dict>
</dict> <key>CFBundleTypeIconFiles</key>
<!-- TXN file type --> <array/>
<dict> <key>CFBundleTypeName</key>
<key>CFBundleTypeIconFiles</key> <string>TXN</string>
<array/> <key>CFBundleTypeRole</key>
<key>CFBundleTypeName</key> <string>Editor</string>
<string>TXN</string> <key>LSHandlerRank</key>
<key>CFBundleTypeRole</key> <string>Owner</string>
<string>Editor</string> <key>LSItemContentTypes</key>
<key>LSHandlerRank</key> <array>
<string>Owner</string> <string>io.bluewallet.psbt.txn</string>
<key>LSItemContentTypes</key> </array>
<array> </dict>
<string>io.bluewallet.psbt.txn</string> <dict>
</array> <key>CFBundleTypeIconFiles</key>
</dict> <array/>
<!-- Electrum Backup file type --> <key>CFBundleTypeName</key>
<dict> <string>ELECTRUMBACKUP</string>
<key>CFBundleTypeIconFiles</key> <key>CFBundleTypeRole</key>
<array/> <string>Editor</string>
<key>CFBundleTypeName</key> <key>LSHandlerRank</key>
<string>ELECTRUMBACKUP</string> <string>Owner</string>
<key>CFBundleTypeRole</key> <key>LSItemContentTypes</key>
<string>Editor</string> <array>
<key>LSHandlerRank</key> <string>io.bluewallet.backup</string>
<string>Owner</string> </array>
<key>LSItemContentTypes</key> </dict>
<array> <dict>
<string>io.bluewallet.backup</string> <key>CFBundleTypeIconFiles</key>
</array> <array/>
</dict> <key>CFBundleTypeName</key>
<!-- BW COSIGNER file type --> <string>BW COSIGNER</string>
<dict> <key>CFBundleTypeRole</key>
<key>CFBundleTypeIconFiles</key> <string>Editor</string>
<array/> <key>LSHandlerRank</key>
<key>CFBundleTypeName</key> <string>Owner</string>
<string>BW COSIGNER</string> <key>LSItemContentTypes</key>
<key>CFBundleTypeRole</key> <array>
<string>Editor</string> <string>io.bluewallet.bwcosigner</string>
<key>LSHandlerRank</key> </array>
<string>Owner</string> </dict>
<key>LSItemContentTypes</key> </array>
<array> <key>CFBundleExecutable</key>
<string>io.bluewallet.bwcosigner</string> <string>$(EXECUTABLE_NAME)</string>
</array> <key>CFBundleIdentifier</key>
</dict> <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
</array> <key>CFBundleInfoDictionaryVersion</key>
<key>CFBundleExecutable</key> <string>6.0</string>
<string>$(EXECUTABLE_NAME)</string> <key>CFBundleName</key>
<key>CFBundleIdentifier</key> <string>$(PRODUCT_NAME)</string>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string> <key>WKCompanionAppBundleIdentifier</key>
<key>CFBundleInfoDictionaryVersion</key> <string>io.bluewallet.bluewallet</string>
<string>6.0</string> <key>CFBundlePackageType</key>
<key>CFBundleName</key> <string>APPL</string>
<string>$(PRODUCT_NAME)</string> <key>CFBundleShortVersionString</key>
<key>CFBundlePackageType</key> <string>$(MARKETING_VERSION)</string>
<string>APPL</string> <key>CFBundleSignature</key>
<key>CFBundleShortVersionString</key> <string>????</string>
<string>$(MARKETING_VERSION)</string> <key>CFBundleURLTypes</key>
<key>CFBundleSignature</key> <array>
<string>????</string> <dict>
<key>CFBundleURLTypes</key> <key>CFBundleTypeRole</key>
<array> <string>Editor</string>
<dict> <key>CFBundleURLSchemes</key>
<key>CFBundleTypeRole</key> <array>
<string>Editor</string> <string>bitcoin</string>
<key>CFBundleURLSchemes</key> <string>lightning</string>
<array> <string>bluewallet</string>
<string>bitcoin</string> <string>lapp</string>
<string>lightning</string> <string>blue</string>
<string>bluewallet</string> </array>
<string>lapp</string> </dict>
<string>blue</string> </array>
</array> <key>CFBundleVersion</key>
</dict> <string>$(CURRENT_PROJECT_VERSION)</string>
</array> <key>ITSAppUsesNonExemptEncryption</key>
<key>CFBundleVersion</key> <false/>
<string>$(CURRENT_PROJECT_VERSION)</string> <key>LSApplicationCategoryType</key>
<key>ITSAppUsesNonExemptEncryption</key> <string>public.app-category.finance</string>
<false/> <key>LSApplicationQueriesSchemes</key>
<key>LSApplicationCategoryType</key> <array>
<string>public.app-category.finance</string> <string>https</string>
<key>LSApplicationQueriesSchemes</key> <string>http</string>
<array> </array>
<string>https</string> <key>LSRequiresIPhoneOS</key>
<string>http</string> <true/>
</array> <key>LSSupportsOpeningDocumentsInPlace</key>
<key>LSRequiresIPhoneOS</key> <true/>
<true/> <key>NSAppTransportSecurity</key>
<key>LSSupportsOpeningDocumentsInPlace</key> <dict>
<true/> <key>NSAllowsArbitraryLoads</key>
<key>NSAppTransportSecurity</key> <false/>
<dict> <key>NSAllowsLocalNetworking</key>
<key>NSAllowsArbitraryLoads</key> <true/>
<false/> <key>NSExceptionDomains</key>
<key>NSAllowsLocalNetworking</key> <dict>
<true/> <key>localhost</key>
<key>NSExceptionDomains</key> <dict>
<dict> <key>NSExceptionAllowsInsecureHTTPLoads</key>
<key>localhost</key> <true/>
<dict> </dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key> <key>onion</key>
<true/> <dict>
</dict> <key>NSExceptionAllowsInsecureHTTPLoads</key>
<key>onion</key> <true/>
<dict> <key>NSIncludesSubdomains</key>
<key>NSExceptionAllowsInsecureHTTPLoads</key> <true/>
<true/> </dict>
<key>NSIncludesSubdomains</key> <key>tailscale.net</key>
<true/> <dict>
</dict> <key>NSExceptionAllowsInsecureHTTPLoads</key>
<key>tailscale.net</key> <true/>
<dict> <key>NSIncludesSubdomains</key>
<key>NSExceptionAllowsInsecureHTTPLoads</key> <true/>
<true/> </dict>
<key>NSIncludesSubdomains</key> <key>ts.net</key>
<true/> <dict>
</dict> <key>NSExceptionAllowsInsecureHTTPLoads</key>
<key>ts.net</key> <true/>
<dict> <key>NSIncludesSubdomains</key>
<key>NSExceptionAllowsInsecureHTTPLoads</key> <true/>
<true/> </dict>
<key>NSIncludesSubdomains</key> </dict>
<true/> </dict>
</dict> <key>NSCameraUsageDescription</key>
</dict> <string>In order to quickly scan the recipient&apos;s address, we need your permission to use the camera to scan their QR Code.</string>
</dict> <key>NSFaceIDUsageDescription</key>
<key>NSCameraUsageDescription</key> <string>In order to use FaceID please confirm your permission.</string>
<string>In order to quickly scan the recipient's address, we need your permission to use the camera to scan their QR Code.</string> <key>NSPhotoLibraryAddUsageDescription</key>
<key>NSFaceIDUsageDescription</key> <string>Your authorization is required to save this image.</string>
<string>In order to use FaceID please confirm your permission.</string> <key>NSPhotoLibraryUsageDescription</key>
<key>NSPhotoLibraryAddUsageDescription</key> <string>In order to import an image for scanning, we need your permission to access your photo library.</string>
<string>Your authorization is required to save this image.</string> <key>NSUserActivityTypes</key>
<key>NSPhotoLibraryUsageDescription</key> <array>
<string>In order to import an image for scanning, we need your permission to access your photo library.</string> <string>io.bluewallet.bluewallet.receiveonchain</string>
<key>NSUserActivityTypes</key> <string>io.bluewallet.bluewallet.xpub</string>
<array> </array>
<string>io.bluewallet.bluewallet.receiveonchain</string> <key>UIAppFonts</key>
<string>io.bluewallet.bluewallet.xpub</string> <array>
</array> <string>Entypo.ttf</string>
<key>UIAppFonts</key> <string>FontAwesome.ttf</string>
<array> <string>FontAwesome5_Brands.ttf</string>
<string>Entypo.ttf</string> <string>FontAwesome5_Regular.ttf</string>
<string>FontAwesome.ttf</string> <string>FontAwesome5_Solid.ttf</string>
<string>FontAwesome5_Brands.ttf</string> <string>Ionicons.ttf</string>
<string>FontAwesome5_Regular.ttf</string> <string>MaterialIcons.ttf</string>
<string>FontAwesome5_Solid.ttf</string> <string>Octicons.ttf</string>
<string>Ionicons.ttf</string> </array>
<string>MaterialIcons.ttf</string> <key>UIBackgroundModes</key>
<string>Octicons.ttf</string> <array>
</array> <string>fetch</string>
<key>UIBackgroundModes</key> <string>processing</string>
<array> <string>remote-notification</string>
<string>fetch</string> </array>
<string>processing</string> <key>UIFileSharingEnabled</key>
<string>remote-notification</string> <true/>
</array> <key>UILaunchStoryboardName</key>
<key>UIFileSharingEnabled</key> <string>LaunchScreen</string>
<true/> <key>NSAppIntents</key>
<key>UILaunchStoryboardName</key> <array>
<string>LaunchScreen</string> <dict>
<key>NSAppIntents</key> <key>INIntentClassName</key>
<array> <string>PriceView</string>
<dict> <key>IntentName</key>
<key>INIntentClassName</key> <string>Bitcoin Price</string>
<string>PriceView</string> <key>IntentDescription</key>
<key>IntentName</key> <string>Quickly view the current Bitcoin market rate.</string>
<string>Bitcoin Price</string> </dict>
<key>IntentDescription</key> </array>
<string>Quickly view the current Bitcoin market rate.</string> <key>UIRequiredDeviceCapabilities</key>
</dict> <array>
</array> <string>arm64</string>
<key>UIRequiredDeviceCapabilities</key> </array>
<array> <key>UISupportedInterfaceOrientations</key>
<string>arm64</string> <array>
</array> <string>UIInterfaceOrientationPortrait</string>
<key>UISupportedInterfaceOrientations</key> <string>UIInterfaceOrientationPortraitUpsideDown</string>
<array> </array>
<string>UIInterfaceOrientationPortrait</string> <key>UISupportedInterfaceOrientations~ipad</key>
<string>UIInterfaceOrientationPortraitUpsideDown</string> <array>
</array> <string>UIInterfaceOrientationPortrait</string>
<key>UISupportedInterfaceOrientations~ipad</key> <string>UIInterfaceOrientationLandscapeLeft</string>
<array> <string>UIInterfaceOrientationLandscapeRight</string>
<string>UIInterfaceOrientationPortrait</string> <string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string> </array>
<string>UIInterfaceOrientationLandscapeRight</string> <key>UIViewControllerBasedStatusBarAppearance</key>
<string>UIInterfaceOrientationPortraitUpsideDown</string> <true/>
</array> <key>UTExportedTypeDeclarations</key>
<key>UIViewControllerBasedStatusBarAppearance</key> <array>
<true/> <dict>
<key>UTTypeConformsTo</key>
<!-- Define exported types (UTIs) for file types --> <array>
<key>UTExportedTypeDeclarations</key> <string>public.data</string>
<array> </array>
<!-- PSBT --> <key>UTTypeDescription</key>
<dict> <string>Partially Signed Bitcoin Transaction</string>
<key>UTTypeConformsTo</key> <key>UTTypeIdentifier</key>
<array> <string>io.bluewallet.psbt</string>
<string>public.data</string> <key>UTTypeTagSpecification</key>
</array> <dict>
<key>UTTypeDescription</key> <key>public.filename-extension</key>
<string>Partially Signed Bitcoin Transaction</string> <array>
<key>UTTypeIdentifier</key> <string>psbt</string>
<string>io.bluewallet.psbt</string> </array>
<key>UTTypeTagSpecification</key> </dict>
<dict> </dict>
<key>public.filename-extension</key> <dict>
<array> <key>UTTypeDescription</key>
<string>psbt</string> <string>BW COSIGNER</string>
</array> <key>UTTypeIdentifier</key>
</dict> <string>io.bluewallet.bwcosigner</string>
</dict> <key>UTTypeTagSpecification</key>
<!-- BW Cosigner --> <dict>
<dict> <key>public.filename-extension</key>
<key>UTTypeDescription</key> <array>
<string>BW COSIGNER</string> <string>bwcosigner</string>
<key>UTTypeIdentifier</key> </array>
<string>io.bluewallet.bwcosigner</string> </dict>
<key>UTTypeTagSpecification</key> </dict>
<dict> <dict>
<key>public.filename-extension</key> <key>UTTypeConformsTo</key>
<array> <array>
<string>bwcosigner</string> <string>public.data</string>
</array> </array>
</dict> <key>UTTypeDescription</key>
</dict> <string>Bitcoin Transaction</string>
<!-- TXN --> <key>UTTypeIdentifier</key>
<dict> <string>io.bluewallet.psbt.txn</string>
<key>UTTypeConformsTo</key> <key>UTTypeTagSpecification</key>
<array> <dict>
<string>public.data</string> <key>public.filename-extension</key>
</array> <array>
<key>UTTypeDescription</key> <string>txn</string>
<string>Bitcoin Transaction</string> </array>
<key>UTTypeIdentifier</key> </dict>
<string>io.bluewallet.psbt.txn</string> </dict>
<key>UTTypeTagSpecification</key> <dict>
<dict> <key>UTTypeConformsTo</key>
<key>public.filename-extension</key> <array>
<array> <string>public.data</string>
<string>txn</string> </array>
</array> <key>UTTypeDescription</key>
</dict> <string>Electrum Backup</string>
</dict> <key>UTTypeIdentifier</key>
<!-- Electrum Backup --> <string>io.bluewallet.backup</string>
<dict> <key>UTTypeTagSpecification</key>
<key>UTTypeConformsTo</key> <dict>
<array> <key>public.filename-extension</key>
<string>public.data</string> <array>
</array> <string>backup</string>
<key>UTTypeDescription</key> </array>
<string>Electrum Backup</string> </dict>
<key>UTTypeIdentifier</key> </dict>
<string>io.bluewallet.backup</string> </array>
<key>UTTypeTagSpecification</key> <key>UTImportedTypeDeclarations</key>
<dict> <array>
<key>public.filename-extension</key> <dict>
<array> <key>UTTypeConformsTo</key>
<string>backup</string> <array>
</array> <string>public.text</string>
</dict> </array>
</dict> <key>UTTypeDescription</key>
</array> <string>JSON File</string>
<key>UTTypeIdentifier</key>
<!-- Define imported types for other files --> <string>public.json</string>
<key>UTImportedTypeDeclarations</key> <key>UTTypeTagSpecification</key>
<array> <dict>
<dict> <key>public.filename-extension</key>
<key>UTTypeConformsTo</key> <array>
<array> <string>json</string>
<string>public.text</string> </array>
</array> <key>public.mime-type</key>
<key>UTTypeDescription</key> <array>
<string>JSON File</string> <string>application/json</string>
<key>UTTypeIdentifier</key> </array>
<string>public.json</string> </dict>
<key>UTTypeTagSpecification</key> <key>LSHandlerRank</key>
<dict> <string>Alternate</string>
<key>public.filename-extension</key> </dict>
<array> </array>
<string>json</string> <key>bugsnag</key>
</array> <dict>
<key>public.mime-type</key> <key>apiKey</key>
<array> <string>17ba9059f676f1cc4f45d98182388b01</string>
<string>application/json</string> </dict>
</array> <key>FIREBASE_ANALYTICS_COLLECTION_ENABLED</key>
</dict> <false/>
<key>LSHandlerRank</key> <key>FIREBASE_MESSAGING_AUTO_INIT_ENABLED</key>
<string>Alternate</string> <false/>
</dict>
</array>
<key>bugsnag</key>
<dict>
<key>apiKey</key>
<string>17ba9059f676f1cc4f45d98182388b01</string>
</dict>
<key>FIREBASE_ANALYTICS_COLLECTION_ENABLED</key>
<false/>
<key>FIREBASE_MESSAGING_AUTO_INIT_ENABLED</key>
<false/>
</dict> </dict>
</plist> </plist>

View file

@ -56,5 +56,7 @@
<key>apiKey</key> <key>apiKey</key>
<string>17ba9059f676f1cc4f45d98182388b01</string> <string>17ba9059f676f1cc4f45d98182388b01</string>
</dict> </dict>
<key>WKCompanionAppBundleIdentifier</key>
<string>io.bluewallet.bluewallet</string>
</dict> </dict>
</plist> </plist>

View file

@ -9,30 +9,15 @@ import WatchKit
import WatchConnectivity import WatchConnectivity
import Foundation import Foundation
class InterfaceController: WKInterfaceController, WCSessionDelegate { class InterfaceController: WKInterfaceController {
@IBOutlet weak var walletsTable: WKInterfaceTable! @IBOutlet weak var walletsTable: WKInterfaceTable!
@IBOutlet weak var noWalletsAvailableLabel: WKInterfaceLabel! @IBOutlet weak var noWalletsAvailableLabel: WKInterfaceLabel!
override func awake(withContext context: Any?) {
setupSession()
}
override func willActivate() { override func willActivate() {
super.willActivate() super.willActivate()
updateUI() updateUI()
NotificationCenter.default.addObserver(self, selector: #selector(updateUI), name: WatchDataSource.NotificationName.dataUpdated, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(updateUI), name: Notifications.dataUpdated.name, object: nil)
}
private func setupSession() {
guard WCSession.isSupported() else { return }
WCSession.default.delegate = self
WCSession.default.activate()
}
private func processContextData(_ context: Any?) {
guard let contextUnwrapped = context as? [String: Any] else { return }
WatchDataSource.shared.processData(data: contextUnwrapped)
} }
@objc private func updateUI() { @objc private func updateUI() {
@ -58,24 +43,4 @@ class InterfaceController: WKInterfaceController, WCSessionDelegate {
return rowIndex return rowIndex
} }
func session(_ session: WCSession, didReceiveApplicationContext applicationContext: [String : Any]) {
WatchDataSource.shared.processData(data: applicationContext)
}
func session(_ session: WCSession, didReceiveUserInfo userInfo: [String : Any] = [:]) {
WatchDataSource.shared.processData(data: userInfo)
}
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
if activationState == .activated {
WatchDataSource.shared.loadKeychainData()
}
}
func session(_ session: WCSession, didReceiveMessage message: [String : Any]) {
WatchDataSource.shared.processData(data: message)
}
} }

View file

@ -1,41 +1,59 @@
//
// Wallet.swift
// BlueWalletWatch Extension
//
// Created by Marcos Rodriguez on 3/13/19.
//
import Foundation import Foundation
class Transaction: NSObject, NSSecureCoding { /// Represents a transaction with various properties including its type.
static var supportsSecureCoding: Bool = true /// Conforms to `Codable` and `Identifiable` for encoding/decoding and unique identification.
struct Transaction: Codable, Identifiable, Equatable {
let id: UUID
let time: Date
let memo: String
let type: TransactionType
let amount: Decimal
static let identifier: String = "Transaction" /// Initializes a new Transaction instance.
/// - Parameters:
let time: String /// - id: Unique identifier for the transaction. Defaults to a new UUID.
let memo: String /// - time: Timestamp of the transaction.
let amount: String /// - memo: A memo or note associated with the transaction.
let type: String /// - type: The type of the transaction, defined by `TransactionType`.
/// - amount: The amount involved in the transaction as a string.
init(time: String, memo: String, type: String, amount: String) { init(id: UUID = UUID(), time: Date, memo: String, type: TransactionType, amount: Decimal) {
self.time = time self.id = id
self.memo = memo self.time = time
self.type = type self.memo = memo
self.amount = amount self.type = type
} self.amount = amount
}
func encode(with aCoder: NSCoder) { }
aCoder.encode(time, forKey: "time")
aCoder.encode(memo, forKey: "memo") extension Transaction {
aCoder.encode(type, forKey: "type") static var mock: Transaction {
aCoder.encode(amount, forKey: "amount") Transaction(
} time: Date(timeIntervalSince1970: 1714398896), // 2024-04-27T12:34:56Z
memo: "Mock Transaction",
required init?(coder aDecoder: NSCoder) { type: .sent,
time = aDecoder.decodeObject(forKey: "time") as! String amount: Decimal(string: "-0.001")!
memo = aDecoder.decodeObject(forKey: "memo") as! String )
amount = aDecoder.decodeObject(forKey: "amount") as! String }
type = aDecoder.decodeObject(forKey: "type") as! String
} static var mockTransactions: [Transaction] {
[
.mock,
Transaction(
time: Date(timeIntervalSince1970: 1714308153), // 2024-04-26T11:22:33Z
memo: "Another Mock Transaction",
type: .received,
amount: Decimal(string: "0.002")!
),
Transaction(
time: Date(timeIntervalSince1970: 1714217482), // 2024-04-25T10:11:22Z
memo: "Third Mock Transaction",
type: .pending,
amount: Decimal.zero
)
]
}
func formattedAmount(for unit: BitcoinUnit) -> String {
return amount.formatted(as: unit)
}
} }

View file

@ -31,7 +31,7 @@ class TransactionTableRow: NSObject {
var time: String = "" { var time: String = "" {
willSet { willSet {
if type == "pendingConfirmation" { if type == .pending || type == .pending_transaction {
transactionTimeLabel.setText("Pending...") transactionTimeLabel.setText("Pending...")
} else { } else {
transactionTimeLabel.setText(newValue) transactionTimeLabel.setText(newValue)
@ -39,13 +39,13 @@ class TransactionTableRow: NSObject {
} }
} }
var type: String = "" { var type: TransactionType = .pending {
willSet { willSet {
if (newValue == "pendingConfirmation") { if (newValue == .pending_transaction || newValue == .pending) {
transactionTypeImage.setImage(UIImage(named: "pendingConfirmation")) transactionTypeImage.setImage(UIImage(named: "pendingConfirmation"))
} else if (newValue == "received") { } else if (newValue == .received) {
transactionTypeImage.setImage(UIImage(named: "receivedArrow")) transactionTypeImage.setImage(UIImage(named: "receivedArrow"))
} else if (newValue == "sent") { } else if (newValue == .sent) {
transactionTypeImage.setImage(UIImage(named: "sentArrow")) transactionTypeImage.setImage(UIImage(named: "sentArrow"))
} else { } else {
transactionTypeImage.setImage(nil) transactionTypeImage.setImage(nil)

View file

@ -0,0 +1,177 @@
//
// TransactionType.swift
// BlueWallet
//
// Created by Marcos Rodriguez on 11/20/24.
// Copyright © 2024 BlueWallet. All rights reserved.
//
// Models/TransactionType.swift
import Foundation
/// Represents the various types of transactions available in the application.
/// Conforms to `String`, `Codable`, `Equatable`, and `CustomStringConvertible` for easy encoding/decoding, comparisons, and descriptions.
enum TransactionType: Codable, Equatable, CustomStringConvertible {
// Transaction direction
case outgoing
case incoming
// Transaction state
case pending
case expired
// Transaction type
case onchain
case offchain
// Fallback
case unknown(String) // For any unknown or future transaction types
}
// MARK: - Coding Keys
enum CodingKeys: String, CodingKey {
case rawValue = "type"
}
// MARK: - Decodable Conformance
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let typeString = try container.decode(String.self, forKey: .rawValue)
self = TransactionType(rawString: typeString)
}
// MARK: - Encodable Conformance
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.rawString, forKey: .rawValue)
}
// MARK: - Custom Initializer
/// Initializes a `TransactionType` from a raw string.
/// - Parameter rawString: The raw string representing the transaction type.
init(rawString: String) {
switch rawString.lowercased() {
case "sent":
self = .sent
case "received":
self = .received
case "pending":
self = .pending
case "pending_transaction":
self = .pending_transaction
case "bitcoind_tx":
self = .onchain
case "paid_invoice":
self = .offchain
case "expired_transaction":
self = .expired_transaction
case "incoming_transaction":
self = .incoming_transaction
case "outgoing_transaction":
self = .outgoing_transaction
default:
self = .unknown(rawString)
}
}
// MARK: - Computed Property for Raw String
/// Returns the raw string associated with the `TransactionType`.
var rawString: String {
switch self {
case .sent:
return "sent"
case .received:
return "received"
case .pending:
return "pending"
case .pending_transaction:
return "pending_transaction"
case .onchain:
return "bitcoind_tx"
case .offchain:
return "paid_invoice"
case .expired_transaction:
return "expired_transaction"
case .incoming_transaction:
return "incoming_transaction"
case .outgoing_transaction:
return "outgoing_transaction"
case .unknown(let typeString):
return typeString
}
}
// MARK: - Description
/// Provides a user-friendly description of the `TransactionType`.
var description: String {
switch self {
case .sent:
return "Sent"
case .received:
return "Received"
case .pending:
return "pending"
case .pending_transaction:
return "Pending Transaction"
case .onchain:
return "Onchain"
case .offchain:
return "Offchain"
case .expired_transaction:
return "Expired Transaction"
case .incoming_transaction:
return "Incoming Transaction"
case .outgoing_transaction:
return "Outgoing Transaction"
case .unknown(let typeString):
return typeString
}
}
// MARK: - Helper Function to Convert Raw String to TransactionType
/// Attempts to convert a raw string to its corresponding `TransactionType`.
/// - Parameter typeString: The raw string representing the transaction type.
/// - Returns: A `TransactionType` instance.
static func fromRawString(_ typeString: String) -> TransactionType {
return TransactionType(rawString: typeString)
}
}
// MARK: - Computed Properties for Categorizing Transaction Types
extension TransactionType {
var isIncoming: Bool {
switch self {
case .received, .incoming_transaction:
return true
default:
return false
}
}
var isOutgoing: Bool {
switch self {
case .sent, .outgoing_transaction:
return true
default:
return false
}
}
var isPending: Bool {
switch self {
case .pending, .pending_transaction, .expired_transaction:
return true
default:
return false
}
}
static var mockSent: TransactionType {
return .sent
}
static var mockReceived: TransactionType {
return .received
}
}

View file

@ -1,66 +1,66 @@
//
// Wallet.swift
// BlueWalletWatch Extension
//
// Created by Marcos Rodriguez on 3/13/19.
//
import Foundation import Foundation
class Wallet: NSObject, NSSecureCoding { /// Represents a wallet with various properties including its type.
/// Conforms to `Codable` and `Identifiable` for encoding/decoding and unique identification.
static var supportsSecureCoding: Bool = true struct Wallet: Codable, Identifiable, Equatable {
let id: UUID
static let identifier: String = "Wallet" let label: String
let balance: String
var identifier: Int? let type: WalletType
let label: String let chain: Chain
let balance: String let preferredBalanceUnit: BitcoinUnit
let type: String let receiveAddress: String
let preferredBalanceUnit: String let transactions: [Transaction]
let receiveAddress: String let xpub: String
let transactions: [Transaction] let hideBalance: Bool
let xpub: String? let paymentCode: String?
let hideBalance: Bool
let paymentCode: String?
init(label: String, balance: String, type: String, preferredBalanceUnit: String, receiveAddress: String, transactions: [Transaction], identifier: Int, xpub: String?, hideBalance: Bool = false, paymentCode: String?) {
self.label = label
self.balance = balance
self.type = type
self.preferredBalanceUnit = preferredBalanceUnit
self.receiveAddress = receiveAddress
self.transactions = transactions
self.identifier = identifier
self.xpub = xpub
self.hideBalance = hideBalance
self.paymentCode = paymentCode
}
func encode(with aCoder: NSCoder) {
aCoder.encode(label, forKey: "label")
aCoder.encode(balance, forKey: "balance")
aCoder.encode(type, forKey: "type")
aCoder.encode(receiveAddress, forKey: "receiveAddress")
aCoder.encode(preferredBalanceUnit, forKey: "preferredBalanceUnit")
aCoder.encode(transactions, forKey: "transactions")
aCoder.encode(identifier, forKey: "identifier")
aCoder.encode(xpub, forKey: "xpub")
aCoder.encode(hideBalance, forKey: "hideBalance")
aCoder.encode(paymentCode, forKey: "paymentCode")
}
required init?(coder aDecoder: NSCoder) {
label = aDecoder.decodeObject(forKey: "label") as! String
balance = aDecoder.decodeObject(forKey: "balance") as! String
type = aDecoder.decodeObject(forKey: "type") as! String
preferredBalanceUnit = aDecoder.decodeObject(forKey: "preferredBalanceUnit") as! String
receiveAddress = aDecoder.decodeObject(forKey: "receiveAddress") as! String
transactions = aDecoder.decodeObject(forKey: "transactions") as? [Transaction] ?? [Transaction]()
xpub = aDecoder.decodeObject(forKey: "xpub") as? String
hideBalance = aDecoder.decodeObject(forKey: "hideBalance") as? Bool ?? false
paymentCode = aDecoder.decodeObject(forKey: "paymentCode") as? String
}
/// Initializes a new Wallet instance.
/// - Parameters:
/// - id: Unique identifier for the wallet. Defaults to a new UUID.
/// - label: Display label for the wallet.
/// - balance: Current balance of the wallet as a string.
/// - type: The type of the wallet, defined by `WalletType`.
/// - preferredBalanceUnit: The preferred unit for displaying balance (e.g., BTC).
/// - receiveAddress: The address to receive funds.
/// - transactions: An array of transactions associated with the wallet.
/// - xpub: Extended public key for HD wallets.
/// - hideBalance: Indicates whether the balance should be hidden.
/// - paymentCode: Optional payment code associated with the wallet.
init(id: UUID = UUID(), label: String, balance: String, type: WalletType, chain: Chain = .onchain, preferredBalanceUnit: BitcoinUnit = .sats, receiveAddress: String, transactions: [Transaction], xpub: String, hideBalance: Bool, paymentCode: String? = nil) {
self.id = id
self.label = label
self.balance = balance
self.type = type
self.chain = chain
self.preferredBalanceUnit = preferredBalanceUnit
self.receiveAddress = receiveAddress
self.transactions = transactions
self.xpub = xpub
self.hideBalance = hideBalance
self.paymentCode = paymentCode
}
}
extension Wallet {
static var mock: Wallet {
Wallet(
label: "Mock Wallet",
balance: "1.2345 BTC",
type: .hdSegwitBech32Wallet,
preferredBalanceUnit: .sats,
receiveAddress: "bc1qmockaddressxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
transactions: Transaction.mockTransactions, // Includes multiple transactions
xpub: "xpub6CUGRUonZSQ4TWtTMmzXdrXDtypWKiKp...",
hideBalance: false,
paymentCode: "p2pkh_mock_payment_code"
)
}
}
extension Wallet {
var formattedBalance: String {
guard let balanceDecimal = Decimal(string: balance) else { return balance }
return balanceDecimal.formatted(as: preferredBalanceUnit)
}
} }

View file

@ -1,35 +1,128 @@
// import SwiftUI
// WalletGradient.swift
// BlueWalletWatch Extension
//
// Created by Marcos Rodriguez on 3/23/19.
// struct WalletGradient {
import Foundation static let hdSegwitP2SHWallet: [Color] = [
Color( "#007AFF"),
Color( "#0040FF")
]
enum WalletGradient: String { static let hdSegwitBech32Wallet: [Color] = [
case SegwitHD = "HDsegwitP2SH" Color( "#6CD9FC"),
case Segwit = "segwitP2SH" Color( "#44BEE5")
case LightningCustodial = "lightningCustodianWallet" ]
case SegwitNative = "HDsegwitBech32"
case WatchOnly = "watchOnly"
case MultiSig = "HDmultisig"
var imageString: String{ static let segwitBech32Wallet: [Color] = [
switch self { Color( "#6CD9FC"),
case .Segwit: Color( "#44BEE5")
]
static let watchOnlyWallet: [Color] = [
Color( "#474646"),
Color( "#282828")
]
static let legacyWallet: [Color] = [
Color( "#37E8C0"),
Color( "#15BE98")
]
static let hdLegacyP2PKHWallet: [Color] = [
Color( "#FD7478"),
Color( "#E73B40")
]
static let hdLegacyBreadWallet: [Color] = [
Color( "#FE6381"),
Color( "#F99C42")
]
static let multisigHdWallet: [Color] = [
Color( "#1CE6EB"),
Color( "#296FC5"),
Color( "#3500A2")
]
static let defaultGradients: [Color] = [
Color( "#B770F6"),
Color( "#9013FE")
]
static let lightningCustodianWallet: [Color] = [
Color( "#F1AA07"),
Color( "#FD7E37")
]
static let aezeedWallet: [Color] = [
Color( "#8584FF"),
Color( "#5351FB")
]
// MARK: - Gradient Selection
static func gradientsFor(type: WalletType) -> [Color] {
switch type {
case .watchOnlyWallet:
return WalletGradient.watchOnlyWallet
case .legacyWallet:
return WalletGradient.legacyWallet
case .hdLegacyP2PKHWallet:
return WalletGradient.hdLegacyP2PKHWallet
case .hdLegacyBreadWallet:
return WalletGradient.hdLegacyBreadWallet
case .hdSegwitP2SHWallet:
return WalletGradient.hdSegwitP2SHWallet
case .hdSegwitBech32Wallet:
return WalletGradient.hdSegwitBech32Wallet
case .segwitBech32Wallet:
return WalletGradient.segwitBech32Wallet
case .multisigHdWallet:
return WalletGradient.multisigHdWallet
case .aezeedWallet:
return WalletGradient.aezeedWallet
case .lightningCustodianWallet:
return WalletGradient.lightningCustodianWallet
default:
return WalletGradient.defaultGradients
}
}
// MARK: - Header Color Selection
/// Returns the primary color for headers based on the wallet type.
/// Typically, the first color of the gradient is used for headers.
/// - Parameter type: The type of the wallet.
/// - Returns: A `Color` representing the header color.
static func headerColorFor(type: WalletType) -> Color {
let gradient = gradientsFor(type: type)
return gradient.first ?? Color.black // Defaults to black if gradient is empty
}
static func imageStringFor(type: WalletType) -> String {
switch type {
case .hdSegwitP2SHWallet:
return "wallet" return "wallet"
case .SegwitNative: case .segwitBech32Wallet:
return "walletHDSegwitNative" return "walletHDSegwitNative"
case .SegwitHD: case .hdSegwitBech32Wallet:
return "walletHD" return "walletHD"
case .WatchOnly: case .watchOnlyWallet:
return "walletWatchOnly" return "walletWatchOnly"
case .LightningCustodial: case .lightningCustodianWallet:
return "walletLightningCustodial" return "walletLightningCustodial"
case .MultiSig: case .multisigHdWallet:
return "watchMultisig" return "watchMultisig"
case .legacyWallet:
return "walletLegacy"
case .hdLegacyP2PKHWallet:
return "walletHDLegacyP2PKH"
case .hdLegacyBreadWallet:
return "walletHDLegacyBread"
case .aezeedWallet:
return "walletAezeed"
case .defaultGradients:
return "walletLegacy"
} }
} }
} }

View file

@ -14,6 +14,7 @@ class WalletInformation: NSObject {
@IBOutlet private weak var walletNameLabel: WKInterfaceLabel! @IBOutlet private weak var walletNameLabel: WKInterfaceLabel!
@IBOutlet private weak var walletGroup: WKInterfaceGroup! @IBOutlet private weak var walletGroup: WKInterfaceGroup!
static let identifier: String = "WalletInformation" static let identifier: String = "WalletInformation"
let type: Wallet? = nil
var name: String = "" { var name: String = "" {
willSet { willSet {
@ -27,11 +28,7 @@ class WalletInformation: NSObject {
} }
} }
var type: WalletGradient = .SegwitHD {
willSet {
walletGroup.setBackgroundImageNamed(newValue.imageString)
}
}
} }
@ -41,6 +38,5 @@ extension WalletInformation {
walletBalanceLabel.setHidden(wallet.hideBalance) walletBalanceLabel.setHidden(wallet.hideBalance)
name = wallet.label name = wallet.label
balance = wallet.hideBalance ? "" : wallet.balance balance = wallet.hideBalance ? "" : wallet.balance
type = WalletGradient(rawValue: wallet.type) ?? .SegwitHD
} }
} }

View file

@ -0,0 +1,198 @@
//
// WalletType.swift
// BlueWallet
//
// Created by Marcos Rodriguez on 11/20/24.
// Copyright © 2024 BlueWallet. All rights reserved.
//
import Foundation
/// Represents the various types of wallets available in the application.
/// Conforms to `Codable` and `Equatable`, handling encoding and decoding for known and unknown types.
enum WalletType: Codable, Equatable {
case hdSegwitP2SHWallet
case hdSegwitBech32Wallet
case segwitBech32Wallet
case watchOnlyWallet
case legacyWallet
case hdLegacyP2PKHWallet
case hdLegacyBreadWallet
case multisigHdWallet
case lightningCustodianWallet
case aezeedWallet
case defaultGradients
// MARK: - Coding Keys
enum CodingKeys: String, CodingKey {
case rawValue = "type"
}
// MARK: - Decodable Conformance
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let typeString = try container.decode(String.self, forKey: .rawValue)
switch typeString {
case "HDsegwitP2SH":
self = .hdSegwitP2SHWallet
case "HDsegwitBech32":
self = .hdSegwitBech32Wallet
case "segwitBech32":
self = .segwitBech32Wallet
case "watchOnly":
self = .watchOnlyWallet
case "legacy":
self = .legacyWallet
case "HDLegacyP2PKH":
self = .hdLegacyP2PKHWallet
case "HDLegacyBreadwallet":
self = .hdLegacyBreadWallet
case "HDmultisig":
self = .multisigHdWallet
case "LightningCustodianWallet":
self = .lightningCustodianWallet
case "HDAezeedWallet":
self = .aezeedWallet
default:
self = .defaultGradients
}
}
// MARK: - Encodable Conformance
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
switch self {
case .hdSegwitP2SHWallet:
try container.encode("HDsegwitP2SH", forKey: .rawValue)
case .hdSegwitBech32Wallet:
try container.encode("HDsegwitBech32", forKey: .rawValue)
case .segwitBech32Wallet:
try container.encode("segwitBech32", forKey: .rawValue)
case .watchOnlyWallet:
try container.encode("watchOnly", forKey: .rawValue)
case .legacyWallet:
try container.encode("legacy", forKey: .rawValue)
case .hdLegacyP2PKHWallet:
try container.encode("HDLegacyP2PKH", forKey: .rawValue)
case .hdLegacyBreadWallet:
try container.encode("HDLegacyBreadwallet", forKey: .rawValue)
case .multisigHdWallet:
try container.encode("HDmultisig", forKey: .rawValue)
case .lightningCustodianWallet:
try container.encode("LightningCustodianWallet", forKey: .rawValue)
case .aezeedWallet:
try container.encode("HDAezeedWallet", forKey: .rawValue)
case .defaultGradients:
try container.encode("DefaultGradients", forKey: .rawValue)
}
}
// MARK: - Custom Initializer from Raw String
/// Initializes a `WalletType` from a raw string.
/// - Parameter rawString: The raw string representing the wallet type.
init(rawString: String) {
self = WalletType.fromRawString(rawString)
}
// MARK: - Helper Method to Convert Raw String to WalletType
/// Attempts to convert a raw string to its corresponding `WalletType`.
/// - Parameter typeString: The raw string representing the wallet type.
/// - Returns: A `WalletType` instance.
static func fromRawString(_ typeString: String) -> WalletType {
switch typeString {
case "HDsegwitP2SH":
return .hdSegwitP2SHWallet
case "HDsegwitBech32":
return .hdSegwitBech32Wallet
case "segwitBech32":
return .segwitBech32Wallet
case "watchOnly":
return .watchOnlyWallet
case "legacy":
return .legacyWallet
case "HDLegacyP2PKH":
return .hdLegacyP2PKHWallet
case "HDLegacyBreadwallet":
return .hdLegacyBreadWallet
case "HDmultisig":
return .multisigHdWallet
case "LightningCustodianWallet":
return .lightningCustodianWallet
case "HDAezeedWallet":
return .aezeedWallet
case "DefaultGradients":
return .defaultGradients
default:
return .defaultGradients
}
}
// MARK: - Computed Property for Raw String
/// Returns the raw string associated with the `WalletType`.
var rawString: String {
switch self {
case .hdSegwitP2SHWallet:
return "HDsegwitP2SH"
case .hdSegwitBech32Wallet:
return "HDsegwitBech32"
case .segwitBech32Wallet:
return "segwitBech32"
case .watchOnlyWallet:
return "watchOnly"
case .legacyWallet:
return "legacy"
case .hdLegacyP2PKHWallet:
return "HDLegacyP2PKH"
case .hdLegacyBreadWallet:
return "HDLegacyBreadwallet"
case .multisigHdWallet:
return "HDmultisig"
case .lightningCustodianWallet:
return "LightningCustodianWallet"
case .aezeedWallet:
return "HDAezeedWallet"
case .defaultGradients:
return "DefaultGradients"
}
}
}
// MARK: - CustomStringConvertible Conformance
extension WalletType: CustomStringConvertible {
/// Provides a user-friendly description of the `WalletType`.
var description: String {
switch self {
case .hdSegwitP2SHWallet:
return "HD Segwit P2SH Wallet"
case .hdSegwitBech32Wallet:
return "HD Segwit Bech32 Wallet"
case .segwitBech32Wallet:
return "Segwit Bech32 Wallet"
case .watchOnlyWallet:
return "Watch Only Wallet"
case .legacyWallet:
return "Legacy Wallet"
case .hdLegacyP2PKHWallet:
return "HD Legacy P2PKH Wallet"
case .hdLegacyBreadWallet:
return "HD Legacy Bread Wallet"
case .multisigHdWallet:
return "Multisig HD Wallet"
case .lightningCustodianWallet:
return "Lightning Custodian Wallet"
case .aezeedWallet:
return "Aezeed Wallet"
case .defaultGradients:
return "Default Gradients"
}
}
}
extension WalletType {
static var mockType: WalletType {
return .hdSegwitBech32Wallet
}
}

View file

@ -1,125 +1,361 @@
// // Data/WatchDataSource.swift
// WatchDataSource.swift
// BlueWalletWatch Extension
//
// Created by Marcos Rodriguez on 3/20/19.
//
import Foundation import Foundation
import WatchConnectivity import WatchConnectivity
import KeychainSwift import Security
import Combine
import ClockKit
class WatchDataSource: NSObject { struct NotificationName {
struct NotificationName { static let dataUpdated = Notification.Name(rawValue: "Notification.WalletDataSource.Updated")
static let dataUpdated = Notification.Name(rawValue: "Notification.WalletDataSource.Updated") }
} struct Notifications {
struct Notifications { static let dataUpdated = Notification(name: NotificationName.dataUpdated)
static let dataUpdated = Notification(name: NotificationName.dataUpdated) }
}
static let shared = WatchDataSource() /// Represents the group user defaults keys.
var wallets: [Wallet] = [Wallet]() /// Ensure these match the keys used in your iOS app for sharing data.
var companionWalletsInitialized = false
private let keychain = KeychainSwift()
let groupUserDefaults = UserDefaults(suiteName: UserDefaultsGroupKey.GroupName.rawValue)
/// Handles WatchConnectivity and data synchronization between iOS and Watch apps.
override init() { class WatchDataSource: NSObject, ObservableObject, WCSessionDelegate {
super.init() // MARK: - Singleton Instance
loadKeychainData()
}
func loadKeychainData() {
if let existingData = keychain.getData(Wallet.identifier), let walletData = try? NSKeyedUnarchiver.unarchivedArrayOfObjects(ofClass: Wallet.self, from: existingData) {
guard walletData != self.wallets else { return }
wallets = walletData
WatchDataSource.postDataUpdatedNotification()
}
}
func processWalletsData(walletsInfo: [String: Any]) {
if let walletsToProcess = walletsInfo["wallets"] as? [[String: Any]] {
wallets.removeAll();
for (index, entry) in walletsToProcess.enumerated() {
guard let label = entry["label"] as? String, let balance = entry["balance"] as? String, let type = entry["type"] as? String, let preferredBalanceUnit = entry["preferredBalanceUnit"] as? String, let transactions = entry["transactions"] as? [[String: Any]], let paymentCode = entry["paymentCode"] as? String else {
continue
}
var transactionsProcessed = [Transaction]()
for transactionEntry in transactions {
guard let time = transactionEntry["time"] as? String, let memo = transactionEntry["memo"] as? String, let amount = transactionEntry["amount"] as? String, let type = transactionEntry["type"] as? String else { continue }
let transaction = Transaction(time: time, memo: memo, type: type, amount: amount)
transactionsProcessed.append(transaction)
}
let receiveAddress = entry["receiveAddress"] as? String ?? ""
let xpub = entry["xpub"] as? String ?? ""
let hideBalance = entry["hideBalance"] as? Bool ?? false
let wallet = Wallet(label: label, balance: balance, type: type, preferredBalanceUnit: preferredBalanceUnit, receiveAddress: receiveAddress, transactions: transactionsProcessed, identifier: index, xpub: xpub, hideBalance: hideBalance, paymentCode: paymentCode)
wallets.append(wallet)
}
if let walletsArchived = try? NSKeyedArchiver.archivedData(withRootObject: wallets, requiringSecureCoding: false) {
keychain.set(walletsArchived, forKey: Wallet.identifier)
}
WatchDataSource.postDataUpdatedNotification()
}
}
static func postDataUpdatedNotification() { static func postDataUpdatedNotification() {
NotificationCenter.default.post(Notifications.dataUpdated) NotificationCenter.default.post(Notifications.dataUpdated)
}
static func requestLightningInvoice(walletIdentifier: Int, amount: Double, description: String?, responseHandler: @escaping (_ invoice: String) -> Void) {
guard WatchDataSource.shared.wallets.count > walletIdentifier else {
responseHandler("")
return
} }
WCSession.default.sendMessage(["request": "createInvoice", "walletIndex": walletIdentifier, "amount": amount, "description": description ?? ""], replyHandler: { (reply: [String : Any]) in
if let invoicePaymentRequest = reply["invoicePaymentRequest"] as? String, !invoicePaymentRequest.isEmpty {
responseHandler(invoicePaymentRequest)
} else {
responseHandler("")
}
}) { (error) in
print(error)
responseHandler("")
static let shared = WatchDataSource()
// MARK: - Published Properties
/// The list of wallets to be displayed on the Watch app.
@Published var wallets: [Wallet] = []
@Published var isDataLoaded: Bool = false
// MARK: - Private Properties
private let groupUserDefaults = UserDefaults(suiteName: UserDefaultsGroupKey.GroupName.rawValue)
private let keychain = KeychainHelper.shared
private let session: WCSession
private var cancellables = Set<AnyCancellable>()
// MARK: - Initializer
private override init() {
guard WCSession.isSupported() else {
print("WCSession is not supported on this device.")
// Initialize with a default session but mark as unsupported
self.session = WCSession.default
super.init()
return
}
self.session = WCSession.default
super.init()
self.session.delegate = self
loadKeychainData()
setupBindings()
} }
}
static func toggleWalletHideBalance(walletIdentifier: Int, hideBalance: Bool, responseHandler: @escaping (_ invoice: String) -> Void) { // MARK: - Public Methods
guard WatchDataSource.shared.wallets.count > walletIdentifier else {
responseHandler("") /// Starts the WatchConnectivity session.
return func startSession() {
// Check if keychain has existing wallets data before activating session
if let existingData = keychain.retrieve(service: UserDefaultsGroupKey.WatchAppBundleIdentifier.rawValue, account: UserDefaultsGroupKey.BundleIdentifier.rawValue),
!existingData.isEmpty {
session.activate()
} else {
print("Keychain is empty. Skipping WCSession activation.")
}
} }
WCSession.default.sendMessage(["message": "hideBalance", "walletIndex": walletIdentifier, "hideBalance": hideBalance], replyHandler: { (reply: [String : Any]) in
responseHandler("")
}) { (error) in
print(error)
responseHandler("")
/// Deactivates the WatchConnectivity session (if needed).
/// Note: WCSession does not provide a deactivate method, but you can handle any necessary cleanup here.
func deactivateSession() {
// Handle any necessary cleanup here.
} }
}
// MARK: - Data Binding
func processData(data: [String: Any]) { /// Sets up bindings to observe changes to `wallets` and perform actions accordingly.
private func setupBindings() {
// Observe changes to wallets and perform actions if needed.
$wallets
.sink { [weak self] updatedWallets in
self?.saveWalletsToKeychain()
self?.reloadComplications()
}
.store(in: &cancellables)
}
if let preferredFiatCurrency = data["preferredFiatCurrency"] as? String, let preferredFiatCurrencyUnit = fiatUnit(currency: preferredFiatCurrency) { // MARK: - Keychain Operations
groupUserDefaults?.set(preferredFiatCurrencyUnit.endPointKey, forKey: "preferredCurrency")
groupUserDefaults?.synchronize()
// Create an instance of ExtensionDelegate and call updatePreferredFiatCurrency() /// Loads wallets data from the Keychain asynchronously.
let extensionDelegate = ExtensionDelegate() private func loadKeychainData() {
extensionDelegate.updatePreferredFiatCurrency() DispatchQueue.global(qos: .background).async { [weak self] in
guard let self = self else { return }
guard let existingData = self.keychain.retrieve(service: UserDefaultsGroupKey.WatchAppBundleIdentifier.rawValue, account: UserDefaultsGroupKey.BundleIdentifier.rawValue),
let decodedWallets = try? JSONDecoder().decode([Wallet].self, from: existingData) else {
print("No existing wallets data found in Keychain.")
return
}
} else if let isWalletsInitialized = data["isWalletsInitialized"] as? Bool { // Filter wallets to include only on-chain wallets.
companionWalletsInitialized = isWalletsInitialized let onChainWallets = decodedWallets.filter { $0.chain == .onchain }
NotificationCenter.default.post(Notifications.dataUpdated)
} else { DispatchQueue.main.async {
WatchDataSource.shared.processWalletsData(walletsInfo: data) if onChainWallets != self.wallets {
} self.wallets = onChainWallets
} print("Loaded \(onChainWallets.count) on-chain wallets from Keychain.")
}
self.isDataLoaded = true
}
}
}
/// Saves the current wallets data to the Keychain asynchronously.
private func saveWalletsToKeychain() {
DispatchQueue.global(qos: .background).async { [weak self] in
guard let self = self else { return }
guard self.session.isReachable || self.session.activationState == .activated else {
print("iPhone is not reachable or session is not active. Skipping save to Keychain.")
return
}
guard let encodedData = try? JSONEncoder().encode(self.wallets) else {
print("Failed to encode wallets.")
return
}
let success = self.keychain.save(encodedData, service: UserDefaultsGroupKey.WatchAppBundleIdentifier.rawValue, account: UserDefaultsGroupKey.BundleIdentifier.rawValue)
if success {
print("Successfully saved wallets to Keychain.")
} else {
print("Failed to save wallets to Keychain.")
}
}
}
// MARK: - WatchConnectivity Methods
/// Handles the activation completion of the WCSession.
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
if let error = error {
print("WCSession activation failed with error: \(error.localizedDescription)")
} else {
print("WCSession activated with state: \(activationState.rawValue)")
// Request current wallets data from iOS app.
}
}
/// Handles received messages from the iOS app.
func session(_ session: WCSession, didReceiveMessage message: [String : Any], replyHandler: @escaping ([String : Any]) -> Void) {
processReceivedData(message)
}
/// Handles received application context updates from the iOS app.
func session(_ session: WCSession, didReceiveApplicationContext applicationContext: [String : Any]) {
if applicationContext.isEmpty { return }
processReceivedData(applicationContext)
}
// MARK: - Data Processing
/// Processes received data from the iOS app.
/// - Parameter data: The data received either as a message or application context.
private func processReceivedData(_ data: [String: Any]) {
if let preferredFiatCurrency = data["preferredFiatCurrency"] as? String {
// Handle preferred fiat currency update.
groupUserDefaults?.set(preferredFiatCurrency, forKey: "preferredCurrency")
// Fetch and update market data based on the new preferred currency.
updateMarketData(for: preferredFiatCurrency)
} else {
// Assume the data contains wallets information.
processWalletsData(walletsInfo: data)
}
}
/// Processes wallets data received from the iOS app.
/// - Parameter walletsInfo: The wallets data received as a dictionary.
private func processWalletsData(walletsInfo: [String: Any]) {
guard let walletsToProcess = walletsInfo["wallets"] as? [[String: Any]] else {
print("No wallets data found in received context.")
return
}
var processedWallets: [Wallet] = []
for entry in walletsToProcess {
guard let label = entry["label"] as? String,
let balance = entry["balance"] as? Double,
let typeString = entry["type"] as? String,
let preferredBalanceUnitString = entry["preferredBalanceUnit"] as? String,
let chainString = entry["chain"] as? String,
let transactions = entry["transactions"] as? [[String: Any]] else {
print("Incomplete wallet entry found. Skipping.")
continue
}
var transactionsProcessed: [Transaction] = []
for transactionEntry in transactions {
guard let time = transactionEntry["time"] as? String,
let memo = transactionEntry["memo"] as? String,
let amount = transactionEntry["amount"] as? Double,
let type = transactionEntry["type"] as? String else {
print("Incomplete transaction entry found. Skipping.")
continue
}
let transactionType = TransactionType(rawString: type)
let transaction = Transaction(time: time, memo: memo, type: transactionType, amount: "\(amount) BTC")
transactionsProcessed.append(transaction)
}
let receiveAddress = entry["receiveAddress"] as? String ?? ""
let xpub = entry["xpub"] as? String ?? ""
let hideBalance = entry["hideBalance"] as? Bool ?? false
let paymentCode = entry["paymentCode"] as? String
let chain = Chain(rawString: chainString)
let wallet = Wallet(
label: label,
balance: "\(balance) BTC",
type: WalletType(rawString: typeString),
chain: chain,
preferredBalanceUnit: BitcoinUnit(rawString: preferredBalanceUnitString),
receiveAddress: receiveAddress,
transactions: transactionsProcessed,
xpub: xpub,
hideBalance: hideBalance,
paymentCode: paymentCode
)
processedWallets.append(wallet)
}
// Update the published `wallets` property on the main thread.
DispatchQueue.main.async { [weak self] in
self?.wallets = processedWallets
print("Updated wallets from received context.")
WatchDataSource.postDataUpdatedNotification()
}
}
/// Fetches market data based on the preferred fiat currency.
/// - Parameter fiatCurrency: The preferred fiat currency string.
private func updateMarketData(for fiatCurrency: String) {
guard !fiatCurrency.isEmpty else {
print("Invalid fiat currency provided")
return
}
MarketAPI.fetchPrice(currency: fiatCurrency) { [weak self] (marketData, error) in
guard let self = self else { return }
if let error = error {
print("Failed to fetch market data: \(error.localizedDescription)")
// Consider implementing retry logic or fallback mechanism
return
}
guard let marketData = marketData as? MarketData else {
print("Invalid market data format received")
return
}
do {
let widgetData = WidgetDataStore(rate: "\(marketData.rate)", lastUpdate: marketData.dateString, rateDouble: marketData.rate)
if let encodedData = try? JSONEncoder().encode(widgetData) {
self.groupUserDefaults?.set(encodedData, forKey: MarketData.string)
print("Market data updated for currency: \(fiatCurrency)")
} else {
throw NSError(domain: "WatchDataSource", code: 1, userInfo: [NSLocalizedDescriptionKey: "Failed to encode market data"])
}
} catch {
print("Failed to process market data: \(error.localizedDescription)")
}
}
}
// MARK: - Wallet Actions
/// Requests a Lightning Invoice from the iOS app.
/// - Parameters:
/// - walletIdentifier: The index of the wallet in the `wallets` array.
/// - amount: The amount for the invoice.
/// - description: An optional description for the invoice.
/// - responseHandler: A closure to handle the invoice string received from the iOS app.
func requestLightningInvoice(walletIdentifier: Int, amount: Double, description: String?, responseHandler: @escaping (_ invoice: String) -> Void) {
let timeoutSeconds = 30.0
let timeoutTimer = Timer.scheduledTimer(withTimeInterval: timeoutSeconds, repeats: false) { _ in
print("Lightning invoice request timed out")
responseHandler("")
}
guard wallets.indices.contains(walletIdentifier) else {
timeoutTimer.invalidate()
responseHandler("")
return
}
let message: [String: Any] = [
"request": "createInvoice",
"walletIndex": walletIdentifier,
"amount": amount,
"description": description ?? ""
]
session.sendMessage(message, replyHandler: { reply in
timeoutTimer.invalidate()
if let invoicePaymentRequest = reply["invoicePaymentRequest"] as? String, !invoicePaymentRequest.isEmpty {
responseHandler(invoicePaymentRequest)
} else {
responseHandler("")
}
}, errorHandler: { error in
timeoutTimer.invalidate()
print("Error requesting Lightning Invoice: \(error.localizedDescription)")
responseHandler("")
})
}
/// Toggles the visibility of the wallet's balance.
/// - Parameters:
/// - walletIdentifier: The index of the wallet in the `wallets` array.
/// - hideBalance: A boolean indicating whether to hide the balance.
/// - responseHandler: A closure to handle the success status.
func toggleWalletHideBalance(walletIdentifier: UUID, hideBalance: Bool, responseHandler: @escaping (_ success: Bool) -> Void) {
guard wallets.indices.contains(walletIdentifier.hashValue) else {
responseHandler(false)
return
}
let message: [String: Any] = [
"message": "hideBalance",
"walletIndex": walletIdentifier,
"hideBalance": hideBalance
]
session.sendMessage(message, replyHandler: { reply in
responseHandler(true)
}, errorHandler: { error in
print("Error toggling hide balance: \(error.localizedDescription)")
responseHandler(false)
})
}
// MARK: - Complications Reload
/// Reloads all active complications on the Watch face.
private func reloadComplications() {
let server = CLKComplicationServer.sharedInstance()
server.activeComplications?.forEach { complication in
server.reloadTimeline(for: complication)
print("[Complication] Reloaded timeline for \(complication.family.rawValue)")
}
}
} }
extension WatchDataSource {
static var mock: WatchDataSource {
let mockDataSource = WatchDataSource()
mockDataSource.wallets = [Wallet.mock]
return mockDataSource
}
}

View file

@ -43,8 +43,8 @@ class ReceiveInterfaceController: WKInterfaceController {
} }
private func setupView() { private func setupView() {
if receiveMethod == .CreateInvoice && (wallet?.type == WalletGradient.LightningCustodial.rawValue) { if receiveMethod == .CreateInvoice && (wallet?.type == .lightningCustodianWallet) {
presentController(withName: SpecifyInterfaceController.identifier, context: wallet?.identifier) presentController(withName: SpecifyInterfaceController.identifier, context: wallet?.id)
} else { } else {
setupQRCode() setupQRCode()
setupMenuItems() setupMenuItems()
@ -87,12 +87,12 @@ class ReceiveInterfaceController: WKInterfaceController {
override func didAppear() { override func didAppear() {
super.didAppear() super.didAppear()
if isCreatingInvoice() { if isCreatingInvoice() {
presentController(withName: SpecifyInterfaceController.identifier, context: wallet?.identifier) presentController(withName: SpecifyInterfaceController.identifier, context: wallet?.id)
} }
} }
private func isCreatingInvoice() -> Bool { private func isCreatingInvoice() -> Bool {
return receiveMethod == .CreateInvoice && (wallet?.type == WalletGradient.LightningCustodial.rawValue) return receiveMethod == .CreateInvoice && (wallet?.type == .lightningCustodianWallet)
} }
override func didDeactivate() { override func didDeactivate() {

View file

@ -40,7 +40,7 @@ class SpecifyInterfaceController: WKInterfaceController {
let wallet = WatchDataSource.shared.wallets[identifier] let wallet = WatchDataSource.shared.wallets[identifier]
self.wallet = wallet self.wallet = wallet
self.createButton.setAlpha(0.5) self.createButton.setAlpha(0.5)
self.specifiedQRContent.bitcoinUnit = (wallet.type == WalletGradient.LightningCustodial.rawValue) ? .SATS : .BTC self.specifiedQRContent.bitcoinUnit = (wallet.type == .lightningCustodianWallet) ? .SATS : .BTC
NotificationCenter.default.addObserver(forName: NumericKeypadInterfaceController.NotificationName.keypadDataChanged, object: nil, queue: nil) { [weak self] (notification) in NotificationCenter.default.addObserver(forName: NumericKeypadInterfaceController.NotificationName.keypadDataChanged, object: nil, queue: nil) { [weak self] (notification) in
guard let amountObject = notification.object as? [String], !amountObject.isEmpty else { return } guard let amountObject = notification.object as? [String], !amountObject.isEmpty else { return }
if amountObject.count == 1 && (amountObject.first == "." || amountObject.first == "0") { if amountObject.count == 1 && (amountObject.first == "." || amountObject.first == "0") {
@ -60,7 +60,7 @@ class SpecifyInterfaceController: WKInterfaceController {
var isShouldCreateButtonBeEnabled = amountDouble > 0 && !title.isEmpty var isShouldCreateButtonBeEnabled = amountDouble > 0 && !title.isEmpty
if (wallet.type == WalletGradient.LightningCustodial.rawValue) && !WCSession.default.isReachable { if (wallet.type == .lightningCustodianWallet) && !WCSession.default.isReachable {
isShouldCreateButtonBeEnabled = false isShouldCreateButtonBeEnabled = false
} }
@ -89,14 +89,8 @@ class SpecifyInterfaceController: WKInterfaceController {
} }
@IBAction func createButtonTapped() { @IBAction func createButtonTapped() {
if WatchDataSource.shared.companionWalletsInitialized { NotificationCenter.default.post(name: NotificationName.createQRCode, object: specifiedQRContent)
NotificationCenter.default.post(name: NotificationName.createQRCode, object: specifiedQRContent) dismiss()
dismiss()
} else {
presentAlert(withTitle: "Error", message: "Unable to create invoice. Please open BlueWallet on your iPhone and unlock your wallets.", preferredStyle: .alert, actions: [WKAlertAction(title: "OK", style: .default, handler: { [weak self] in
self?.dismiss()
})])
}
} }
override func contextForSegue(withIdentifier segueIdentifier: String) -> Any? { override func contextForSegue(withIdentifier segueIdentifier: String) -> Any? {

View file

@ -21,15 +21,16 @@ class WalletDetailsInterfaceController: WKInterfaceController {
override func awake(withContext context: Any?) { override func awake(withContext context: Any?) {
super.awake(withContext: context) super.awake(withContext: context)
guard let identifier = context as? Int else { guard let identifier = context as? UUID else {
pop() pop()
return return
} }
loadWalletDetails(identifier: identifier) loadWalletDetails(identifier: identifier)
} }
private func loadWalletDetails(identifier: Int) { private func loadWalletDetails(identifier: UUID) {
let wallet = WatchDataSource.shared.wallets[identifier] let index = WatchDataSource.shared.wallets.firstIndex(where: { $0.id == identifier }) ?? 0
let wallet = WatchDataSource.shared.wallets[index]
self.wallet = wallet self.wallet = wallet
updateWalletUI(wallet: wallet) updateWalletUI(wallet: wallet)
updateTransactionsTable(forWallet: wallet) updateTransactionsTable(forWallet: wallet)
@ -39,16 +40,16 @@ class WalletDetailsInterfaceController: WKInterfaceController {
walletBalanceLabel.setHidden(wallet.hideBalance) walletBalanceLabel.setHidden(wallet.hideBalance)
walletBalanceLabel.setText(wallet.hideBalance ? "" : wallet.balance) walletBalanceLabel.setText(wallet.hideBalance ? "" : wallet.balance)
walletNameLabel.setText(wallet.label) walletNameLabel.setText(wallet.label)
walletBasicsGroup.setBackgroundImageNamed(WalletGradient(rawValue: wallet.type)?.imageString) // walletBasicsGroup.setBackgroundImageNamed(WalletGradient(rawValue: wallet.type)?)
let isLightningWallet = wallet.type == WalletGradient.LightningCustodial.rawValue let isLightningWallet = wallet.type == .lightningCustodianWallet
createInvoiceButton.setHidden(!isLightningWallet) createInvoiceButton.setHidden(!isLightningWallet)
receiveButton.setHidden(wallet.receiveAddress.isEmpty) receiveButton.setHidden(wallet.receiveAddress.isEmpty)
viewXPubButton.setHidden(!isXPubAvailable(wallet: wallet)) viewXPubButton.setHidden(!isXPubAvailable(wallet: wallet))
} }
private func isXPubAvailable(wallet: Wallet) -> Bool { private func isXPubAvailable(wallet: Wallet) -> Bool {
return (wallet.type != WalletGradient.LightningCustodial.rawValue) && !(wallet.xpub ?? "").isEmpty return (wallet.type != .lightningCustodianWallet) && !(wallet.xpub).isEmpty
} }
private func updateTransactionsTable(forWallet wallet: Wallet) { private func updateTransactionsTable(forWallet wallet: Wallet) {
@ -77,8 +78,8 @@ class WalletDetailsInterfaceController: WKInterfaceController {
} }
@objc func showBalanceMenuItemTapped() { @objc func showBalanceMenuItemTapped() {
guard let identifier = wallet?.identifier else { return } guard let identifier = wallet?.id else { return }
WatchDataSource.toggleWalletHideBalance(walletIdentifier: identifier, hideBalance: false) { [weak self] _ in WatchDataSource.shared.toggleWalletHideBalance(walletIdentifier: identifier, hideBalance: false) { [weak self] _ in
DispatchQueue.main.async { DispatchQueue.main.async {
WatchDataSource.postDataUpdatedNotification() WatchDataSource.postDataUpdatedNotification()
self?.loadWalletDetails(identifier: identifier) self?.loadWalletDetails(identifier: identifier)
@ -87,8 +88,8 @@ class WalletDetailsInterfaceController: WKInterfaceController {
} }
@objc func hideBalanceMenuItemTapped() { @objc func hideBalanceMenuItemTapped() {
guard let identifier = wallet?.identifier else { return } guard let identifier = wallet?.id else { return }
WatchDataSource.toggleWalletHideBalance(walletIdentifier: identifier, hideBalance: true) { [weak self] _ in WatchDataSource.shared.toggleWalletHideBalance(walletIdentifier: identifier, hideBalance: true) { [weak self] _ in
DispatchQueue.main.async { DispatchQueue.main.async {
WatchDataSource.postDataUpdatedNotification() WatchDataSource.postDataUpdatedNotification()
self?.loadWalletDetails(identifier: identifier) self?.loadWalletDetails(identifier: identifier)
@ -115,20 +116,13 @@ class WalletDetailsInterfaceController: WKInterfaceController {
} }
@IBAction func createInvoiceTapped() { @IBAction func createInvoiceTapped() {
if WatchDataSource.shared.companionWalletsInitialized { guard let wallet = wallet else { return }
guard let wallet = wallet else { return } pushController(withName: ReceiveInterfaceController.identifier, context: (wallet.id, ReceiveMethod.CreateInvoice))
pushController(withName: ReceiveInterfaceController.identifier, context: (wallet.identifier, ReceiveMethod.CreateInvoice))
} else {
WKInterfaceDevice.current().play(.failure)
presentAlert(withTitle: "Error", message: "Unable to create invoice. Please open BlueWallet on your iPhone and unlock your wallets.", preferredStyle: .alert, actions: [WKAlertAction(title: "OK", style: .default, handler: { [weak self] in
self?.dismiss()
})])
}
} }
override func contextForSegue(withIdentifier segueIdentifier: String) -> Any? { override func contextForSegue(withIdentifier segueIdentifier: String) -> Any? {
guard let wallet = wallet else { return nil } guard let wallet = wallet else { return nil }
return (wallet.identifier, ReceiveMethod.Onchain) return (wallet.id, ReceiveMethod.Onchain)
} }
} }

View file

@ -2,19 +2,20 @@ import Foundation
class Balance { class Balance {
static func formatBalance(_ balance: Decimal, toUnit: BitcoinUnit, withFormatting: Bool = false, completion: @escaping (String) -> Void) { static func formatBalance(_ balance: Decimal, toUnit: BitcoinUnit, withFormatting: Bool = false, completion: @escaping (String) -> Void) {
switch toUnit { switch toUnit {
case .BTC: case .sats:
let value = balance / Decimal(100_000_000) if withFormatting {
completion("\(value) BTC") // Localize unit names as needed. completion(NumberFormatter.localizedString(from: balance as NSNumber, number: .decimal) + " SATS")
case .SATS: } else {
if withFormatting { completion("\(balance) SATS")
completion(NumberFormatter.localizedString(from: balance as NSNumber, number: .decimal) + " SATS")
} else {
completion("\(balance) SATS")
}
case .LOCAL_CURRENCY:
fetchLocalCurrencyEquivalent(satoshi: balance, completion: completion)
} }
case .localCurrency:
fetchLocalCurrencyEquivalent(satoshi: balance, completion: completion)
default:
let value = balance / Decimal(100_000_000)
completion("\(value) BTC") // Localize unit names as needed.
}
} }
private static func fetchLocalCurrencyEquivalent(satoshi: Decimal, completion: @escaping (String) -> Void) { private static func fetchLocalCurrencyEquivalent(satoshi: Decimal, completion: @escaping (String) -> Void) {
@ -33,3 +34,25 @@ class Balance {
} }
} }
} }
extension Decimal {
func formatted(as unit: BitcoinUnit, withFormatting: Bool = false) -> String {
switch unit {
case .sats:
return withFormatting ? NumberFormatter.localizedString(from: self as NSNumber, number: .decimal) + " SATS" : "\(self) SATS"
case .localCurrency:
let userDefaults = UserDefaults(suiteName: UserDefaultsGroupKey.GroupName.rawValue)
if let widgetData = userDefaults?.object(forKey: MarketData.string) as? Data,
let marketData = try? JSONDecoder().decode(MarketData.self, from: widgetData) {
let rate = Decimal(marketData.rate)
let convertedAmount = (self / Decimal(100_000_000)) * rate
return "\(convertedAmount) \(Currency.getUserPreferredCurrency())"
} else {
return "N/A"
}
default:
let value = self / Decimal(100_000_000)
return "\(value) BTC"
}
}
}

View file

@ -5,11 +5,52 @@
// Created by Marcos Rodriguez on 4/14/24. // Created by Marcos Rodriguez on 4/14/24.
// Copyright © 2024 BlueWallet. All rights reserved. // Copyright © 2024 BlueWallet. All rights reserved.
// //
import Foundation import Foundation
enum BitcoinUnit: String { /// Represents the various balance units used in the application.
case BTC = "BTC" /// Conforms to `String`, `Codable`, `Equatable`, and `CustomStringConvertible` for easy encoding/decoding, comparisons, and descriptions.
case SATS = "SATS" enum BitcoinUnit: String, Codable, Equatable, CustomStringConvertible {
case LOCAL_CURRENCY = "LOCAL_CURRENCY" case btc = "BTC"
case sats = "sats"
case localCurrency = "local_currency"
case max = "MAX"
/// Provides a user-friendly description of the `BitcoinUnit`.
var description: String {
switch self {
case .btc:
return "BTC"
case .sats:
return "sats"
case .localCurrency:
return "Local Currency"
case .max:
return "MAX"
}
}
/// Initializes a `BitcoinUnit` from a raw string.
/// - Parameter rawString: The raw string representing the balance unit.
init(rawString: String) {
switch rawString.lowercased() {
case "btc":
self = .sats
case "sats":
self = .sats
case "local_currency":
self = .localCurrency
case "max":
self = .max
default:
// Handle unknown balance units if necessary
// For now, defaulting to .max
self = .max
}
}
}
extension BitcoinUnit {
static var mockUnit: BitcoinUnit {
return .sats
}
} }

47
ios/Shared/Chain.swift Normal file
View file

@ -0,0 +1,47 @@
//
// Chain.swift
// BlueWallet
//
// Created by Marcos Rodriguez on 11/16/24.
// Copyright © 2024 BlueWallet. All rights reserved.
//
import Foundation
/// Represents the chain type for a wallet.
/// Conforms to `String`, `Codable`, `Equatable`, and `CustomStringConvertible` for easy encoding/decoding, comparisons, and descriptions.
enum Chain: String, Codable, Equatable, CustomStringConvertible {
case onchain = "ONCHAIN"
case offchain = "OFFCHAIN"
/// Provides a user-friendly description of the `Chain`.
var description: String {
switch self {
case .onchain:
return "On-chain"
case .offchain:
return "Off-chain"
}
}
/// Initializes a `Chain` from a raw string.
/// - Parameter rawString: The raw string representing the chain type.
init(rawString: String) {
switch rawString.uppercased() {
case "ONCHAIN":
self = .onchain
case "OFFCHAIN":
self = .offchain
default:
// Handle unknown chain types if necessary
// For now, defaulting to .onchain
self = .onchain
}
}
}
extension Chain {
static var mockChain: Chain {
return .onchain
}
}

View file

@ -11,6 +11,8 @@ import Foundation
enum UserDefaultsGroupKey: String { enum UserDefaultsGroupKey: String {
case GroupName = "group.io.bluewallet.bluewallet" case GroupName = "group.io.bluewallet.bluewallet"
case PreferredCurrency = "preferredCurrency" case PreferredCurrency = "preferredCurrency"
case WatchAppBundleIdentifier = "io.bluewallet.bluewallet.watch"
case BundleIdentifier = "io.bluewallet.bluewallet"
case ElectrumSettingsHost = "electrum_host" case ElectrumSettingsHost = "electrum_host"
case ElectrumSettingsTCPPort = "electrum_tcp_port" case ElectrumSettingsTCPPort = "electrum_tcp_port"
case ElectrumSettingsSSLPort = "electrum_ssl_port" case ElectrumSettingsSSLPort = "electrum_ssl_port"

View file

@ -0,0 +1,70 @@
//
// KeychainHelper.swift
// BlueWallet
//
// Created by Marcos Rodriguez on 11/20/24.
// Copyright © 2024 BlueWallet. All rights reserved.
//
import Foundation
import Security
class KeychainHelper {
static let shared = KeychainHelper()
private init() {}
/// Save data to Keychain
func save(_ data: Data, service: String, account: String) -> Bool {
// Create query
let query: [String: Any] = [
kSecClass as String : kSecClassGenericPassword,
kSecAttrService as String : service,
kSecAttrAccount as String : account,
kSecValueData as String : data
]
// Delete any existing item
SecItemDelete(query as CFDictionary)
// Add new item
let status = SecItemAdd(query as CFDictionary, nil)
return status == errSecSuccess
}
/// Retrieve data from Keychain
func retrieve(service: String, account: String) -> Data? {
// Create query
let query: [String: Any] = [
kSecClass as String : kSecClassGenericPassword,
kSecAttrService as String : service,
kSecAttrAccount as String : account,
kSecReturnData as String : true,
kSecMatchLimit as String : kSecMatchLimitOne
]
var dataTypeRef: AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &dataTypeRef)
if status == errSecSuccess {
return dataTypeRef as? Data
} else {
return nil
}
}
/// Delete data from Keychain
func delete(service: String, account: String) -> Bool {
// Create query
let query: [String: Any] = [
kSecClass as String : kSecClassGenericPassword,
kSecAttrService as String : service,
kSecAttrAccount as String : account
]
let status = SecItemDelete(query as CFDictionary)
return status == errSecSuccess
}
}

View file

@ -19,9 +19,9 @@ struct WalletData {
formatter.roundingMode = .up formatter.roundingMode = .up
let value = NSNumber(value: balance / 100000000); let value = NSNumber(value: balance / 100000000);
if let valueString = formatter.string(from: value) { if let valueString = formatter.string(from: value) {
return "\(String(describing: valueString)) \(BitcoinUnit.BTC.rawValue)" return "\(String(describing: valueString)) \(BitcoinUnit.btc.rawValue)"
} else { } else {
return "0 \(BitcoinUnit.BTC.rawValue)" return "0 \(BitcoinUnit.btc.rawValue)"
} }
} }
} }