mirror of
https://github.com/BlueWallet/BlueWallet.git
synced 2025-02-23 23:27:26 +01:00
Merge pull request #7339 from BlueWallet/watfchcon
REF: Foundation for watchOS app
This commit is contained in:
commit
96f599de6f
23 changed files with 1729 additions and 832 deletions
|
@ -1,7 +1,6 @@
|
|||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import {
|
||||
transferCurrentComplicationUserInfo,
|
||||
transferUserInfo,
|
||||
updateApplicationContext,
|
||||
useInstalled,
|
||||
usePaired,
|
||||
|
@ -9,12 +8,13 @@ import {
|
|||
watchEvents,
|
||||
} from 'react-native-watch-connectivity';
|
||||
import { MultisigHDWallet } from '../class';
|
||||
import loc, { formatBalance, transactionTimeToReadable } from '../loc';
|
||||
import loc from '../loc';
|
||||
import { Chain } from '../models/bitcoinUnits';
|
||||
import { FiatUnit } from '../models/fiatUnit';
|
||||
import { useSettings } from '../hooks/context/useSettings';
|
||||
import { useStorage } from '../hooks/context/useStorage';
|
||||
import { isNotificationsEnabled, majorTomToGroundControl } from '../blue_modules/notifications';
|
||||
import { LightningTransaction, Transaction } from '../class/wallets/types';
|
||||
|
||||
interface Message {
|
||||
request?: string;
|
||||
|
@ -35,118 +35,49 @@ interface LightningInvoiceCreateRequest {
|
|||
description?: string;
|
||||
}
|
||||
|
||||
interface Transaction {
|
||||
type: string;
|
||||
amount: string;
|
||||
memo: string;
|
||||
time: string;
|
||||
}
|
||||
|
||||
export function useWatchConnectivity() {
|
||||
const { walletsInitialized, wallets, fetchWalletTransactions, saveToDisk, txMetadata } = useStorage();
|
||||
const { preferredFiatCurrency } = useSettings();
|
||||
const isReachable = useReachability();
|
||||
const isInstalled = useInstalled();
|
||||
const isPaired = usePaired();
|
||||
|
||||
const messagesListenerActive = useRef(false);
|
||||
const lastPreferredCurrency = useRef(FiatUnit.USD.endPointKey);
|
||||
|
||||
// Set up message listener only when conditions are met
|
||||
useEffect(() => {
|
||||
if (!isInstalled || !isPaired || !walletsInitialized || !isReachable) {
|
||||
console.debug('Apple Watch not installed, not paired, or other conditions not met. Exiting message listener setup.');
|
||||
return;
|
||||
}
|
||||
const createContextPayload = () => ({
|
||||
randomID: `${Date.now()}${Math.floor(Math.random() * 1000)}`,
|
||||
});
|
||||
|
||||
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(() => {
|
||||
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]);
|
||||
|
||||
// Update preferred fiat currency to Apple Watch if it changes
|
||||
useEffect(() => {
|
||||
if (!isInstalled || !isPaired || !walletsInitialized || !isReachable || !preferredFiatCurrency) return;
|
||||
|
||||
if (lastPreferredCurrency.current !== preferredFiatCurrency.endPointKey) {
|
||||
try {
|
||||
transferCurrentComplicationUserInfo({ preferredFiatCurrency: preferredFiatCurrency.endPointKey });
|
||||
const currencyPayload = { preferredFiatCurrency: preferredFiatCurrency.endPointKey };
|
||||
transferCurrentComplicationUserInfo(currencyPayload);
|
||||
lastPreferredCurrency.current = preferredFiatCurrency.endPointKey;
|
||||
console.debug('Apple Watch: updated preferred fiat currency');
|
||||
console.debug('Apple Watch: updated preferred fiat currency', currencyPayload);
|
||||
} catch (error) {
|
||||
console.debug('Error updating preferredFiatCurrency on watch:', error);
|
||||
console.error('Error updating preferredFiatCurrency on watch:', error);
|
||||
}
|
||||
} else {
|
||||
console.debug('WatchConnectivity lastPreferredCurrency has not changed');
|
||||
console.debug('WatchConnectivity: preferred currency has not changed');
|
||||
}
|
||||
}, [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(
|
||||
async ({ walletIndex, amount, description = loc.lnd.placeholder }: LightningInvoiceCreateRequest): Promise<string | undefined> => {
|
||||
const wallet = wallets[walletIndex];
|
||||
|
@ -159,53 +90,46 @@ export function useWatchConnectivity() {
|
|||
majorTomToGroundControl([], [decoded.payment_hash], []);
|
||||
return invoiceRequest;
|
||||
}
|
||||
console.debug('Created Lightning invoice:', { invoiceRequest });
|
||||
return invoiceRequest;
|
||||
}
|
||||
} catch (invoiceError) {
|
||||
console.debug('Error creating invoice:', invoiceError);
|
||||
console.error('Error creating invoice:', invoiceError);
|
||||
}
|
||||
}
|
||||
},
|
||||
[wallets],
|
||||
);
|
||||
|
||||
// Construct wallet data to send to the watch, including transaction details
|
||||
const constructWalletsToSendToWatch = useCallback(async () => {
|
||||
if (!Array.isArray(wallets) || !walletsInitialized) return;
|
||||
|
||||
const walletsToProcess = await Promise.allSettled(
|
||||
wallets.map(async wallet => {
|
||||
try {
|
||||
let receiveAddress;
|
||||
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 receiveAddress = wallet.chain === Chain.ONCHAIN ? await wallet.getAddressAsync() : wallet.getAddress();
|
||||
const transactions: Transaction[] = wallet
|
||||
.getTransactions()
|
||||
.slice(0, 10)
|
||||
.map((transaction: any) => ({
|
||||
type: transaction.confirmations ? 'pendingConfirmation' : 'received',
|
||||
amount: formatBalance(transaction.value, wallet.getPreferredBalanceUnit(), true).toString(),
|
||||
memo: txMetadata[transaction.hash]?.memo || transaction.memo || '',
|
||||
time: transactionTimeToReadable(transaction.received),
|
||||
.map((transaction: Transaction & LightningTransaction) => ({
|
||||
type: determineTransactionType(transaction),
|
||||
amount: transaction.value ?? 0,
|
||||
memo:
|
||||
'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(),
|
||||
balance: formatBalance(Number(wallet.getBalance()), wallet.getPreferredBalanceUnit(), true),
|
||||
balance: Number(wallet.getBalance()),
|
||||
type: wallet.type,
|
||||
preferredBalanceUnit: wallet.getPreferredBalanceUnit(),
|
||||
receiveAddress,
|
||||
transactions,
|
||||
hideBalance: wallet.hideBalance,
|
||||
chain: wallet.chain,
|
||||
hideBalance: wallet.hideBalance ? 1 : 0,
|
||||
...(wallet.chain === Chain.ONCHAIN &&
|
||||
wallet.type !== MultisigHDWallet.type && {
|
||||
xpub: wallet.getXpub() || wallet.getSecret(),
|
||||
|
@ -214,13 +138,17 @@ export function useWatchConnectivity() {
|
|||
wallet.isBIP47Enabled() &&
|
||||
'getBIP47PaymentCode' in wallet && { paymentCode: wallet.getBIP47PaymentCode() }),
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Failed to construct wallet:', {
|
||||
walletLabel: wallet.getLabel(),
|
||||
walletType: wallet.type,
|
||||
error,
|
||||
|
||||
console.debug('Constructed wallet data for watch:', {
|
||||
label: walletData.label,
|
||||
type: walletData.type,
|
||||
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)
|
||||
.map(result => (result as PromiseFulfilledResult<any>).value);
|
||||
|
||||
console.debug('Constructed wallets to process for Apple Watch');
|
||||
return { wallets: processedWallets, randomID: Math.floor(Math.random() * 11) };
|
||||
console.debug('Constructed wallets to process for Apple Watch:', {
|
||||
walletCount: processedWallets.length,
|
||||
walletLabels: processedWallets.map(wallet => wallet.label),
|
||||
});
|
||||
return { wallets: processedWallets, randomID: `${Date.now()}${Math.floor(Math.random() * 1000)}` };
|
||||
}, [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;
|
|
@ -125,6 +125,14 @@
|
|||
B4742E992CCDBE8300380EEE /* 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 */; };
|
||||
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 */; };
|
||||
B48630DD2CCEE7AC00A8425C /* 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 */; };
|
||||
B48630ED2CCEEEB000A8425C /* WalletAppShortcuts.swift in Sources */ = {isa = PBXBuildFile; fileRef = B48630EB2CCEEEA700A8425C /* WalletAppShortcuts.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 */; };
|
||||
B49A28BC2CD18999006B08E4 /* CompactPriceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B49A28BA2CD18999006B08E4 /* CompactPriceView.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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
|
@ -450,7 +461,6 @@
|
|||
files = (
|
||||
B41B76852B66B2FF002C48D5 /* Bugsnag in Frameworks */,
|
||||
B41B76872B66B2FF002C48D5 /* BugsnagNetworkRequestPlugin in Frameworks */,
|
||||
B48A6A292C1DF01000030AB9 /* KeychainSwift in Frameworks */,
|
||||
6DFC807024EA0B6C007B8700 /* EFQRCode in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
@ -733,6 +743,8 @@
|
|||
B43D03242258474500FBAA95 /* Objects */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B4793DC02CEDACE700C92C2E /* WalletType.swift */,
|
||||
B4793DBE2CEDACDA00C92C2E /* TransactionType.swift */,
|
||||
B43D0374225847C500FBAA95 /* Transaction.swift */,
|
||||
B43D0375225847C500FBAA95 /* TransactionTableRow.swift */,
|
||||
B43D0376225847C500FBAA95 /* Wallet.swift */,
|
||||
|
@ -750,6 +762,7 @@
|
|||
B44033C82BCC34AC00162242 /* Shared */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B4793DBA2CEDACBD00C92C2E /* Chain.swift */,
|
||||
B450109A2C0FCD7E00619044 /* Utilities */,
|
||||
6D2AA8062568B8E50090B089 /* Fiat */,
|
||||
6D9A2E6A254BAB1B007B5B82 /* MarketAPI.swift */,
|
||||
|
@ -776,6 +789,7 @@
|
|||
B450109A2C0FCD7E00619044 /* Utilities */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B4793DC22CEDAD4400C92C2E /* KeychainHelper.swift */,
|
||||
B450109B2C0FCD8A00619044 /* Utilities.swift */,
|
||||
);
|
||||
path = Utilities;
|
||||
|
@ -934,7 +948,6 @@
|
|||
6DFC806F24EA0B6C007B8700 /* EFQRCode */,
|
||||
B41B76842B66B2FF002C48D5 /* Bugsnag */,
|
||||
B41B76862B66B2FF002C48D5 /* BugsnagNetworkRequestPlugin */,
|
||||
B48A6A282C1DF01000030AB9 /* KeychainSwift */,
|
||||
);
|
||||
productName = "BlueWalletWatch Extension";
|
||||
productReference = B40D4E3C225841ED00428FCC /* BlueWalletWatch Extension.appex */;
|
||||
|
@ -1016,7 +1029,6 @@
|
|||
packageReferences = (
|
||||
6DFC806E24EA0B6C007B8700 /* XCRemoteSwiftPackageReference "EFQRCode" */,
|
||||
B41B76832B66B2FF002C48D5 /* XCRemoteSwiftPackageReference "bugsnag-cocoa" */,
|
||||
B48A6A272C1DF01000030AB9 /* XCRemoteSwiftPackageReference "keychain-swift" */,
|
||||
);
|
||||
productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */;
|
||||
projectDirPath = "";
|
||||
|
@ -1248,6 +1260,7 @@
|
|||
B44034072BCC38A000162242 /* FiatUnit.swift in Sources */,
|
||||
B44034002BCC37F800162242 /* Bundle+decode.swift in Sources */,
|
||||
B44033E22BCC36CB00162242 /* Placeholders.swift in Sources */,
|
||||
B4793DBB2CEDACBD00C92C2E /* Chain.swift in Sources */,
|
||||
B4B1A4622BFA73110072E3BB /* WidgetHelper.swift in Sources */,
|
||||
B48630E12CCEE7C800A8425C /* PriceWidgetEntryView.swift in Sources */,
|
||||
B44033DA2BCC369A00162242 /* Colors.swift in Sources */,
|
||||
|
@ -1256,6 +1269,7 @@
|
|||
B49A28BC2CD18999006B08E4 /* CompactPriceView.swift in Sources */,
|
||||
B44033D82BCC369500162242 /* UserDefaultsExtension.swift in Sources */,
|
||||
B44033E42BCC36FF00162242 /* WalletData.swift in Sources */,
|
||||
B4793DC52CEDAD4400C92C2E /* KeychainHelper.swift in Sources */,
|
||||
B44033BF2BCC32F800162242 /* BitcoinUnit.swift in Sources */,
|
||||
B44034052BCC389200162242 /* XMLParserDelegate.swift in Sources */,
|
||||
B48630DD2CCEE7AC00A8425C /* PriceWidgetEntry.swift in Sources */,
|
||||
|
@ -1270,6 +1284,7 @@
|
|||
6DD410BE266CAF5C0087DE03 /* SendReceiveButtons.swift in Sources */,
|
||||
B48630E02CCEE7C800A8425C /* PriceWidgetEntryView.swift in Sources */,
|
||||
6DD410B4266CAF5C0087DE03 /* MarketAPI.swift in Sources */,
|
||||
B4793DC32CEDAD4400C92C2E /* KeychainHelper.swift in Sources */,
|
||||
B40FC3FA29CCD1D00007EBAC /* SwiftTCPClient.swift in Sources */,
|
||||
B48630EC2CCEEEA700A8425C /* WalletAppShortcuts.swift in Sources */,
|
||||
6DD410A1266CADF10087DE03 /* Widgets.swift in Sources */,
|
||||
|
@ -1291,6 +1306,7 @@
|
|||
B44033F02BCC374500162242 /* Numeric+abbreviated.swift in Sources */,
|
||||
B44033DF2BCC36C300162242 /* LatestTransaction.swift in Sources */,
|
||||
6DD410C0266CB1460087DE03 /* MarketWidget.swift in Sources */,
|
||||
B4793DBC2CEDACBD00C92C2E /* Chain.swift in Sources */,
|
||||
B4AB225E2B02AD12001F4328 /* XMLParserDelegate.swift in Sources */,
|
||||
B44033F62BCC377F00162242 /* WidgetData.swift in Sources */,
|
||||
6DD410BA266CAF5C0087DE03 /* FiatUnit.swift in Sources */,
|
||||
|
@ -1329,6 +1345,7 @@
|
|||
6DFC807224EA2FA9007B8700 /* ViewQRCodefaceController.swift in Sources */,
|
||||
B40D4E46225841ED00428FCC /* NotificationController.swift in Sources */,
|
||||
B40D4E5D2258425500428FCC /* InterfaceController.swift in Sources */,
|
||||
B4793DBD2CEDACBD00C92C2E /* Chain.swift in Sources */,
|
||||
B44033FA2BCC379200162242 /* WidgetDataStore.swift in Sources */,
|
||||
B44033DE2BCC36C300162242 /* LatestTransaction.swift in Sources */,
|
||||
B4D0B2662C1DEB7F006B6B1B /* ReceiveInterfaceMode.swift in Sources */,
|
||||
|
@ -1338,6 +1355,7 @@
|
|||
B40D4E642258425500428FCC /* WalletDetailsInterfaceController.swift in Sources */,
|
||||
B40D4E44225841ED00428FCC /* ExtensionDelegate.swift in Sources */,
|
||||
6D4AF16D25D21192009DD853 /* Placeholders.swift in Sources */,
|
||||
B4793DC42CEDAD4400C92C2E /* KeychainHelper.swift in Sources */,
|
||||
B44033DB2BCC369B00162242 /* Colors.swift in Sources */,
|
||||
B40D4E632258425500428FCC /* ReceiveInterfaceController.swift in Sources */,
|
||||
B43D0378225847C500FBAA95 /* WalletGradient.swift in Sources */,
|
||||
|
@ -1345,6 +1363,8 @@
|
|||
B44033C02BCC32F800162242 /* BitcoinUnit.swift in Sources */,
|
||||
B44033E52BCC36FF00162242 /* WalletData.swift in Sources */,
|
||||
B44033EF2BCC374500162242 /* Numeric+abbreviated.swift in Sources */,
|
||||
B4793DC12CEDACE700C92C2E /* WalletType.swift in Sources */,
|
||||
B4793DBF2CEDACDA00C92C2E /* TransactionType.swift in Sources */,
|
||||
B4D0B2622C1DEA11006B6B1B /* ReceivePageInterfaceController.swift in Sources */,
|
||||
B44033D02BCC352F00162242 /* UserDefaultsGroup.swift in Sources */,
|
||||
B44033C52BCC332400162242 /* Balance.swift in Sources */,
|
||||
|
@ -1447,7 +1467,10 @@
|
|||
);
|
||||
HEADER_SEARCH_PATHS = "$(inherited)";
|
||||
INFOPLIST_FILE = BlueWallet/Info.plist;
|
||||
INFOPLIST_KEY_CLKComplicationPrincipalClass = "$(PRODUCT_BUNDLE_IDENTIFIER).ComplicationController";
|
||||
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;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
|
@ -1502,7 +1525,10 @@
|
|||
"ENABLE_HARDENED_RUNTIME[sdk=macosx*]" = YES;
|
||||
HEADER_SEARCH_PATHS = "$(inherited)";
|
||||
INFOPLIST_FILE = BlueWallet/Info.plist;
|
||||
INFOPLIST_KEY_CLKComplicationPrincipalClass = "$(PRODUCT_BUNDLE_IDENTIFIER).ComplicationController";
|
||||
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;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
|
@ -1886,6 +1912,9 @@
|
|||
"DEVELOPMENT_TEAM[sdk=watchos*]" = A7W54YZ4WU;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
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 = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
|
@ -1936,6 +1965,9 @@
|
|||
"DEVELOPMENT_TEAM[sdk=watchos*]" = A7W54YZ4WU;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
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 = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
|
@ -1986,6 +2018,7 @@
|
|||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
IBSC_MODULE = BlueWalletWatch_Extension;
|
||||
INFOPLIST_FILE = BlueWalletWatch/Info.plist;
|
||||
INFOPLIST_KEY_WKCompanionAppBundleIdentifier = io.bluewallet.bluewallet;
|
||||
LIBRARY_SEARCH_PATHS = (
|
||||
"$(SDKROOT)/usr/lib/swift",
|
||||
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
|
||||
|
@ -2034,6 +2067,7 @@
|
|||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
IBSC_MODULE = BlueWalletWatch_Extension;
|
||||
INFOPLIST_FILE = BlueWalletWatch/Info.plist;
|
||||
INFOPLIST_KEY_WKCompanionAppBundleIdentifier = io.bluewallet.bluewallet;
|
||||
LIBRARY_SEARCH_PATHS = (
|
||||
"$(SDKROOT)/usr/lib/swift",
|
||||
"$(SDKROOT)/System/iOSSupport/usr/lib/swift",
|
||||
|
@ -2133,14 +2167,6 @@
|
|||
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 */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
|
@ -2159,11 +2185,6 @@
|
|||
package = B41B76832B66B2FF002C48D5 /* XCRemoteSwiftPackageReference "bugsnag-cocoa" */;
|
||||
productName = BugsnagNetworkRequestPlugin;
|
||||
};
|
||||
B48A6A282C1DF01000030AB9 /* KeychainSwift */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = B48A6A272C1DF01000030AB9 /* XCRemoteSwiftPackageReference "keychain-swift" */;
|
||||
productName = KeychainSwift;
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
};
|
||||
rootObject = 83CBB9F71A601CBA00E9B192 /* Project object */;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"originHash" : "52530e6b1e3a85c8854952ef703a6d1bbe1acd82713be2b3166476b9b277db23",
|
||||
"originHash" : "89509f555bc90a15b96ca0a326a69850770bdaac04a46f9cf482d81533702e3c",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "bugsnag-cocoa",
|
||||
|
@ -19,15 +19,6 @@
|
|||
"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",
|
||||
"kind" : "remoteSourceControl",
|
||||
|
|
|
@ -2,351 +2,338 @@
|
|||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>11</string>
|
||||
<key>BGTaskSchedulerPermittedIdentifiers</key>
|
||||
<array>
|
||||
<string>io.bluewallet.bluewallet.fetchTxsForWallet</string>
|
||||
</array>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>BlueWallet</string>
|
||||
<key>CFBundleDocumentTypes</key>
|
||||
<array>
|
||||
<!-- PSBT file type -->
|
||||
<dict>
|
||||
<key>CFBundleTypeIconFiles</key>
|
||||
<array/>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>PSBT</string>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Owner</string>
|
||||
<key>LSItemContentTypes</key>
|
||||
<array>
|
||||
<string>io.bluewallet.psbt</string>
|
||||
</array>
|
||||
</dict>
|
||||
<!-- Image file types -->
|
||||
<dict>
|
||||
<key>CFBundleTypeIconFiles</key>
|
||||
<array/>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>Image</string>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Viewer</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Alternate</string>
|
||||
<key>LSItemContentTypes</key>
|
||||
<array>
|
||||
<string>public.jpeg</string>
|
||||
<string>public.image</string>
|
||||
</array>
|
||||
</dict>
|
||||
<!-- TXN file type -->
|
||||
<dict>
|
||||
<key>CFBundleTypeIconFiles</key>
|
||||
<array/>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>TXN</string>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Owner</string>
|
||||
<key>LSItemContentTypes</key>
|
||||
<array>
|
||||
<string>io.bluewallet.psbt.txn</string>
|
||||
</array>
|
||||
</dict>
|
||||
<!-- Electrum Backup file type -->
|
||||
<dict>
|
||||
<key>CFBundleTypeIconFiles</key>
|
||||
<array/>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>ELECTRUMBACKUP</string>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Owner</string>
|
||||
<key>LSItemContentTypes</key>
|
||||
<array>
|
||||
<string>io.bluewallet.backup</string>
|
||||
</array>
|
||||
</dict>
|
||||
<!-- BW COSIGNER file type -->
|
||||
<dict>
|
||||
<key>CFBundleTypeIconFiles</key>
|
||||
<array/>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>BW COSIGNER</string>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Owner</string>
|
||||
<key>LSItemContentTypes</key>
|
||||
<array>
|
||||
<string>io.bluewallet.bwcosigner</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>bitcoin</string>
|
||||
<string>lightning</string>
|
||||
<string>bluewallet</string>
|
||||
<string>lapp</string>
|
||||
<string>blue</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>LSApplicationCategoryType</key>
|
||||
<string>public.app-category.finance</string>
|
||||
<key>LSApplicationQueriesSchemes</key>
|
||||
<array>
|
||||
<string>https</string>
|
||||
<string>http</string>
|
||||
</array>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>LSSupportsOpeningDocumentsInPlace</key>
|
||||
<true/>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<false/>
|
||||
<key>NSAllowsLocalNetworking</key>
|
||||
<true/>
|
||||
<key>NSExceptionDomains</key>
|
||||
<dict>
|
||||
<key>localhost</key>
|
||||
<dict>
|
||||
<key>NSExceptionAllowsInsecureHTTPLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>onion</key>
|
||||
<dict>
|
||||
<key>NSExceptionAllowsInsecureHTTPLoads</key>
|
||||
<true/>
|
||||
<key>NSIncludesSubdomains</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>tailscale.net</key>
|
||||
<dict>
|
||||
<key>NSExceptionAllowsInsecureHTTPLoads</key>
|
||||
<true/>
|
||||
<key>NSIncludesSubdomains</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>ts.net</key>
|
||||
<dict>
|
||||
<key>NSExceptionAllowsInsecureHTTPLoads</key>
|
||||
<true/>
|
||||
<key>NSIncludesSubdomains</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<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>NSFaceIDUsageDescription</key>
|
||||
<string>In order to use FaceID please confirm your permission.</string>
|
||||
<key>NSPhotoLibraryAddUsageDescription</key>
|
||||
<string>Your authorization is required to save this image.</string>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>In order to import an image for scanning, we need your permission to access your photo library.</string>
|
||||
<key>NSUserActivityTypes</key>
|
||||
<array>
|
||||
<string>io.bluewallet.bluewallet.receiveonchain</string>
|
||||
<string>io.bluewallet.bluewallet.xpub</string>
|
||||
</array>
|
||||
<key>UIAppFonts</key>
|
||||
<array>
|
||||
<string>Entypo.ttf</string>
|
||||
<string>FontAwesome.ttf</string>
|
||||
<string>FontAwesome5_Brands.ttf</string>
|
||||
<string>FontAwesome5_Regular.ttf</string>
|
||||
<string>FontAwesome5_Solid.ttf</string>
|
||||
<string>Ionicons.ttf</string>
|
||||
<string>MaterialIcons.ttf</string>
|
||||
<string>Octicons.ttf</string>
|
||||
</array>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>fetch</string>
|
||||
<string>processing</string>
|
||||
<string>remote-notification</string>
|
||||
</array>
|
||||
<key>UIFileSharingEnabled</key>
|
||||
<true/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>NSAppIntents</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>INIntentClassName</key>
|
||||
<string>PriceView</string>
|
||||
<key>IntentName</key>
|
||||
<string>Bitcoin Price</string>
|
||||
<key>IntentDescription</key>
|
||||
<string>Quickly view the current Bitcoin market rate.</string>
|
||||
</dict>
|
||||
</array>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<array>
|
||||
<string>arm64</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<true/>
|
||||
|
||||
<!-- Define exported types (UTIs) for file types -->
|
||||
<key>UTExportedTypeDeclarations</key>
|
||||
<array>
|
||||
<!-- PSBT -->
|
||||
<dict>
|
||||
<key>UTTypeConformsTo</key>
|
||||
<array>
|
||||
<string>public.data</string>
|
||||
</array>
|
||||
<key>UTTypeDescription</key>
|
||||
<string>Partially Signed Bitcoin Transaction</string>
|
||||
<key>UTTypeIdentifier</key>
|
||||
<string>io.bluewallet.psbt</string>
|
||||
<key>UTTypeTagSpecification</key>
|
||||
<dict>
|
||||
<key>public.filename-extension</key>
|
||||
<array>
|
||||
<string>psbt</string>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
<!-- BW Cosigner -->
|
||||
<dict>
|
||||
<key>UTTypeDescription</key>
|
||||
<string>BW COSIGNER</string>
|
||||
<key>UTTypeIdentifier</key>
|
||||
<string>io.bluewallet.bwcosigner</string>
|
||||
<key>UTTypeTagSpecification</key>
|
||||
<dict>
|
||||
<key>public.filename-extension</key>
|
||||
<array>
|
||||
<string>bwcosigner</string>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
<!-- TXN -->
|
||||
<dict>
|
||||
<key>UTTypeConformsTo</key>
|
||||
<array>
|
||||
<string>public.data</string>
|
||||
</array>
|
||||
<key>UTTypeDescription</key>
|
||||
<string>Bitcoin Transaction</string>
|
||||
<key>UTTypeIdentifier</key>
|
||||
<string>io.bluewallet.psbt.txn</string>
|
||||
<key>UTTypeTagSpecification</key>
|
||||
<dict>
|
||||
<key>public.filename-extension</key>
|
||||
<array>
|
||||
<string>txn</string>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
<!-- Electrum Backup -->
|
||||
<dict>
|
||||
<key>UTTypeConformsTo</key>
|
||||
<array>
|
||||
<string>public.data</string>
|
||||
</array>
|
||||
<key>UTTypeDescription</key>
|
||||
<string>Electrum Backup</string>
|
||||
<key>UTTypeIdentifier</key>
|
||||
<string>io.bluewallet.backup</string>
|
||||
<key>UTTypeTagSpecification</key>
|
||||
<dict>
|
||||
<key>public.filename-extension</key>
|
||||
<array>
|
||||
<string>backup</string>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
</array>
|
||||
|
||||
<!-- Define imported types for other files -->
|
||||
<key>UTImportedTypeDeclarations</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>UTTypeConformsTo</key>
|
||||
<array>
|
||||
<string>public.text</string>
|
||||
</array>
|
||||
<key>UTTypeDescription</key>
|
||||
<string>JSON File</string>
|
||||
<key>UTTypeIdentifier</key>
|
||||
<string>public.json</string>
|
||||
<key>UTTypeTagSpecification</key>
|
||||
<dict>
|
||||
<key>public.filename-extension</key>
|
||||
<array>
|
||||
<string>json</string>
|
||||
</array>
|
||||
<key>public.mime-type</key>
|
||||
<array>
|
||||
<string>application/json</string>
|
||||
</array>
|
||||
</dict>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Alternate</string>
|
||||
</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/>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>11</string>
|
||||
<key>BGTaskSchedulerPermittedIdentifiers</key>
|
||||
<array>
|
||||
<string>io.bluewallet.bluewallet.fetchTxsForWallet</string>
|
||||
</array>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>BlueWallet</string>
|
||||
<key>CFBundleDocumentTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeIconFiles</key>
|
||||
<array/>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>PSBT</string>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Owner</string>
|
||||
<key>LSItemContentTypes</key>
|
||||
<array>
|
||||
<string>io.bluewallet.psbt</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleTypeIconFiles</key>
|
||||
<array/>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>Image</string>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Viewer</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Alternate</string>
|
||||
<key>LSItemContentTypes</key>
|
||||
<array>
|
||||
<string>public.jpeg</string>
|
||||
<string>public.image</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleTypeIconFiles</key>
|
||||
<array/>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>TXN</string>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Owner</string>
|
||||
<key>LSItemContentTypes</key>
|
||||
<array>
|
||||
<string>io.bluewallet.psbt.txn</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleTypeIconFiles</key>
|
||||
<array/>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>ELECTRUMBACKUP</string>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Owner</string>
|
||||
<key>LSItemContentTypes</key>
|
||||
<array>
|
||||
<string>io.bluewallet.backup</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleTypeIconFiles</key>
|
||||
<array/>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>BW COSIGNER</string>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Owner</string>
|
||||
<key>LSItemContentTypes</key>
|
||||
<array>
|
||||
<string>io.bluewallet.bwcosigner</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>WKCompanionAppBundleIdentifier</key>
|
||||
<string>io.bluewallet.bluewallet</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>Editor</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>bitcoin</string>
|
||||
<string>lightning</string>
|
||||
<string>bluewallet</string>
|
||||
<string>lapp</string>
|
||||
<string>blue</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>LSApplicationCategoryType</key>
|
||||
<string>public.app-category.finance</string>
|
||||
<key>LSApplicationQueriesSchemes</key>
|
||||
<array>
|
||||
<string>https</string>
|
||||
<string>http</string>
|
||||
</array>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>LSSupportsOpeningDocumentsInPlace</key>
|
||||
<true/>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<false/>
|
||||
<key>NSAllowsLocalNetworking</key>
|
||||
<true/>
|
||||
<key>NSExceptionDomains</key>
|
||||
<dict>
|
||||
<key>localhost</key>
|
||||
<dict>
|
||||
<key>NSExceptionAllowsInsecureHTTPLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>onion</key>
|
||||
<dict>
|
||||
<key>NSExceptionAllowsInsecureHTTPLoads</key>
|
||||
<true/>
|
||||
<key>NSIncludesSubdomains</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>tailscale.net</key>
|
||||
<dict>
|
||||
<key>NSExceptionAllowsInsecureHTTPLoads</key>
|
||||
<true/>
|
||||
<key>NSIncludesSubdomains</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>ts.net</key>
|
||||
<dict>
|
||||
<key>NSExceptionAllowsInsecureHTTPLoads</key>
|
||||
<true/>
|
||||
<key>NSIncludesSubdomains</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<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>NSFaceIDUsageDescription</key>
|
||||
<string>In order to use FaceID please confirm your permission.</string>
|
||||
<key>NSPhotoLibraryAddUsageDescription</key>
|
||||
<string>Your authorization is required to save this image.</string>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>In order to import an image for scanning, we need your permission to access your photo library.</string>
|
||||
<key>NSUserActivityTypes</key>
|
||||
<array>
|
||||
<string>io.bluewallet.bluewallet.receiveonchain</string>
|
||||
<string>io.bluewallet.bluewallet.xpub</string>
|
||||
</array>
|
||||
<key>UIAppFonts</key>
|
||||
<array>
|
||||
<string>Entypo.ttf</string>
|
||||
<string>FontAwesome.ttf</string>
|
||||
<string>FontAwesome5_Brands.ttf</string>
|
||||
<string>FontAwesome5_Regular.ttf</string>
|
||||
<string>FontAwesome5_Solid.ttf</string>
|
||||
<string>Ionicons.ttf</string>
|
||||
<string>MaterialIcons.ttf</string>
|
||||
<string>Octicons.ttf</string>
|
||||
</array>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>fetch</string>
|
||||
<string>processing</string>
|
||||
<string>remote-notification</string>
|
||||
</array>
|
||||
<key>UIFileSharingEnabled</key>
|
||||
<true/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>NSAppIntents</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>INIntentClassName</key>
|
||||
<string>PriceView</string>
|
||||
<key>IntentName</key>
|
||||
<string>Bitcoin Price</string>
|
||||
<key>IntentDescription</key>
|
||||
<string>Quickly view the current Bitcoin market rate.</string>
|
||||
</dict>
|
||||
</array>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<array>
|
||||
<string>arm64</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
</array>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<true/>
|
||||
<key>UTExportedTypeDeclarations</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>UTTypeConformsTo</key>
|
||||
<array>
|
||||
<string>public.data</string>
|
||||
</array>
|
||||
<key>UTTypeDescription</key>
|
||||
<string>Partially Signed Bitcoin Transaction</string>
|
||||
<key>UTTypeIdentifier</key>
|
||||
<string>io.bluewallet.psbt</string>
|
||||
<key>UTTypeTagSpecification</key>
|
||||
<dict>
|
||||
<key>public.filename-extension</key>
|
||||
<array>
|
||||
<string>psbt</string>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>UTTypeDescription</key>
|
||||
<string>BW COSIGNER</string>
|
||||
<key>UTTypeIdentifier</key>
|
||||
<string>io.bluewallet.bwcosigner</string>
|
||||
<key>UTTypeTagSpecification</key>
|
||||
<dict>
|
||||
<key>public.filename-extension</key>
|
||||
<array>
|
||||
<string>bwcosigner</string>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>UTTypeConformsTo</key>
|
||||
<array>
|
||||
<string>public.data</string>
|
||||
</array>
|
||||
<key>UTTypeDescription</key>
|
||||
<string>Bitcoin Transaction</string>
|
||||
<key>UTTypeIdentifier</key>
|
||||
<string>io.bluewallet.psbt.txn</string>
|
||||
<key>UTTypeTagSpecification</key>
|
||||
<dict>
|
||||
<key>public.filename-extension</key>
|
||||
<array>
|
||||
<string>txn</string>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>UTTypeConformsTo</key>
|
||||
<array>
|
||||
<string>public.data</string>
|
||||
</array>
|
||||
<key>UTTypeDescription</key>
|
||||
<string>Electrum Backup</string>
|
||||
<key>UTTypeIdentifier</key>
|
||||
<string>io.bluewallet.backup</string>
|
||||
<key>UTTypeTagSpecification</key>
|
||||
<dict>
|
||||
<key>public.filename-extension</key>
|
||||
<array>
|
||||
<string>backup</string>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
</array>
|
||||
<key>UTImportedTypeDeclarations</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>UTTypeConformsTo</key>
|
||||
<array>
|
||||
<string>public.text</string>
|
||||
</array>
|
||||
<key>UTTypeDescription</key>
|
||||
<string>JSON File</string>
|
||||
<key>UTTypeIdentifier</key>
|
||||
<string>public.json</string>
|
||||
<key>UTTypeTagSpecification</key>
|
||||
<dict>
|
||||
<key>public.filename-extension</key>
|
||||
<array>
|
||||
<string>json</string>
|
||||
</array>
|
||||
<key>public.mime-type</key>
|
||||
<array>
|
||||
<string>application/json</string>
|
||||
</array>
|
||||
</dict>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Alternate</string>
|
||||
</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>
|
||||
</plist>
|
||||
</plist>
|
||||
|
|
|
@ -56,5 +56,7 @@
|
|||
<key>apiKey</key>
|
||||
<string>17ba9059f676f1cc4f45d98182388b01</string>
|
||||
</dict>
|
||||
<key>WKCompanionAppBundleIdentifier</key>
|
||||
<string>io.bluewallet.bluewallet</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
|
@ -9,30 +9,15 @@ import WatchKit
|
|||
import WatchConnectivity
|
||||
import Foundation
|
||||
|
||||
class InterfaceController: WKInterfaceController, WCSessionDelegate {
|
||||
|
||||
class InterfaceController: WKInterfaceController {
|
||||
|
||||
@IBOutlet weak var walletsTable: WKInterfaceTable!
|
||||
@IBOutlet weak var noWalletsAvailableLabel: WKInterfaceLabel!
|
||||
|
||||
override func awake(withContext context: Any?) {
|
||||
setupSession()
|
||||
}
|
||||
|
||||
|
||||
override func willActivate() {
|
||||
super.willActivate()
|
||||
updateUI()
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(updateUI), name: WatchDataSource.NotificationName.dataUpdated, 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)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(updateUI), name: Notifications.dataUpdated.name, object: nil)
|
||||
}
|
||||
|
||||
@objc private func updateUI() {
|
||||
|
@ -58,24 +43,4 @@ class InterfaceController: WKInterfaceController, WCSessionDelegate {
|
|||
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)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,41 +1,59 @@
|
|||
//
|
||||
// Wallet.swift
|
||||
// BlueWalletWatch Extension
|
||||
//
|
||||
// Created by Marcos Rodriguez on 3/13/19.
|
||||
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class Transaction: NSObject, NSSecureCoding {
|
||||
static var supportsSecureCoding: Bool = true
|
||||
|
||||
static let identifier: String = "Transaction"
|
||||
|
||||
let time: String
|
||||
let memo: String
|
||||
let amount: String
|
||||
let type: String
|
||||
|
||||
init(time: String, memo: String, type: String, amount: String) {
|
||||
self.time = time
|
||||
self.memo = memo
|
||||
self.type = type
|
||||
self.amount = amount
|
||||
}
|
||||
|
||||
func encode(with aCoder: NSCoder) {
|
||||
aCoder.encode(time, forKey: "time")
|
||||
aCoder.encode(memo, forKey: "memo")
|
||||
aCoder.encode(type, forKey: "type")
|
||||
aCoder.encode(amount, forKey: "amount")
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
time = aDecoder.decodeObject(forKey: "time") as! String
|
||||
memo = aDecoder.decodeObject(forKey: "memo") as! String
|
||||
amount = aDecoder.decodeObject(forKey: "amount") as! String
|
||||
type = aDecoder.decodeObject(forKey: "type") as! String
|
||||
}
|
||||
/// Represents a transaction with various properties including its type.
|
||||
/// 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
|
||||
|
||||
/// Initializes a new Transaction instance.
|
||||
/// - Parameters:
|
||||
/// - id: Unique identifier for the transaction. Defaults to a new UUID.
|
||||
/// - time: Timestamp of the transaction.
|
||||
/// - memo: A memo or note associated with the transaction.
|
||||
/// - type: The type of the transaction, defined by `TransactionType`.
|
||||
/// - amount: The amount involved in the transaction as a string.
|
||||
init(id: UUID = UUID(), time: Date, memo: String, type: TransactionType, amount: Decimal) {
|
||||
self.id = id
|
||||
self.time = time
|
||||
self.memo = memo
|
||||
self.type = type
|
||||
self.amount = amount
|
||||
}
|
||||
}
|
||||
|
||||
extension Transaction {
|
||||
static var mock: Transaction {
|
||||
Transaction(
|
||||
time: Date(timeIntervalSince1970: 1714398896), // 2024-04-27T12:34:56Z
|
||||
memo: "Mock Transaction",
|
||||
type: .sent,
|
||||
amount: Decimal(string: "-0.001")!
|
||||
)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,7 +31,7 @@ class TransactionTableRow: NSObject {
|
|||
|
||||
var time: String = "" {
|
||||
willSet {
|
||||
if type == "pendingConfirmation" {
|
||||
if type == .pending || type == .pending_transaction {
|
||||
transactionTimeLabel.setText("Pending...")
|
||||
} else {
|
||||
transactionTimeLabel.setText(newValue)
|
||||
|
@ -39,13 +39,13 @@ class TransactionTableRow: NSObject {
|
|||
}
|
||||
}
|
||||
|
||||
var type: String = "" {
|
||||
var type: TransactionType = .pending {
|
||||
willSet {
|
||||
if (newValue == "pendingConfirmation") {
|
||||
if (newValue == .pending_transaction || newValue == .pending) {
|
||||
transactionTypeImage.setImage(UIImage(named: "pendingConfirmation"))
|
||||
} else if (newValue == "received") {
|
||||
} else if (newValue == .received) {
|
||||
transactionTypeImage.setImage(UIImage(named: "receivedArrow"))
|
||||
} else if (newValue == "sent") {
|
||||
} else if (newValue == .sent) {
|
||||
transactionTypeImage.setImage(UIImage(named: "sentArrow"))
|
||||
} else {
|
||||
transactionTypeImage.setImage(nil)
|
||||
|
|
177
ios/BlueWalletWatch Extension/Objects/TransactionType.swift
Normal file
177
ios/BlueWalletWatch Extension/Objects/TransactionType.swift
Normal 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
|
||||
}
|
||||
}
|
|
@ -1,66 +1,66 @@
|
|||
//
|
||||
// Wallet.swift
|
||||
// BlueWalletWatch Extension
|
||||
//
|
||||
// Created by Marcos Rodriguez on 3/13/19.
|
||||
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
class Wallet: NSObject, NSSecureCoding {
|
||||
|
||||
static var supportsSecureCoding: Bool = true
|
||||
|
||||
static let identifier: String = "Wallet"
|
||||
|
||||
var identifier: Int?
|
||||
let label: String
|
||||
let balance: String
|
||||
let type: String
|
||||
let preferredBalanceUnit: String
|
||||
let receiveAddress: String
|
||||
let transactions: [Transaction]
|
||||
let xpub: 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
|
||||
}
|
||||
|
||||
/// Represents a wallet with various properties including its type.
|
||||
/// Conforms to `Codable` and `Identifiable` for encoding/decoding and unique identification.
|
||||
struct Wallet: Codable, Identifiable, Equatable {
|
||||
let id: UUID
|
||||
let label: String
|
||||
let balance: String
|
||||
let type: WalletType
|
||||
let chain: Chain
|
||||
let preferredBalanceUnit: BitcoinUnit
|
||||
let receiveAddress: String
|
||||
let transactions: [Transaction]
|
||||
let xpub: String
|
||||
let hideBalance: Bool
|
||||
let paymentCode: 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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,35 +1,128 @@
|
|||
//
|
||||
// WalletGradient.swift
|
||||
// BlueWalletWatch Extension
|
||||
//
|
||||
// Created by Marcos Rodriguez on 3/23/19.
|
||||
import SwiftUI
|
||||
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum WalletGradient: String {
|
||||
case SegwitHD = "HDsegwitP2SH"
|
||||
case Segwit = "segwitP2SH"
|
||||
case LightningCustodial = "lightningCustodianWallet"
|
||||
case SegwitNative = "HDsegwitBech32"
|
||||
case WatchOnly = "watchOnly"
|
||||
case MultiSig = "HDmultisig"
|
||||
struct WalletGradient {
|
||||
|
||||
var imageString: String{
|
||||
switch self {
|
||||
case .Segwit:
|
||||
static let hdSegwitP2SHWallet: [Color] = [
|
||||
Color( "#007AFF"),
|
||||
Color( "#0040FF")
|
||||
]
|
||||
|
||||
static let hdSegwitBech32Wallet: [Color] = [
|
||||
Color( "#6CD9FC"),
|
||||
Color( "#44BEE5")
|
||||
]
|
||||
|
||||
static let segwitBech32Wallet: [Color] = [
|
||||
Color( "#6CD9FC"),
|
||||
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"
|
||||
case .SegwitNative:
|
||||
case .segwitBech32Wallet:
|
||||
return "walletHDSegwitNative"
|
||||
case .SegwitHD:
|
||||
case .hdSegwitBech32Wallet:
|
||||
return "walletHD"
|
||||
case .WatchOnly:
|
||||
case .watchOnlyWallet:
|
||||
return "walletWatchOnly"
|
||||
case .LightningCustodial:
|
||||
case .lightningCustodianWallet:
|
||||
return "walletLightningCustodial"
|
||||
case .MultiSig:
|
||||
case .multisigHdWallet:
|
||||
return "watchMultisig"
|
||||
case .legacyWallet:
|
||||
return "walletLegacy"
|
||||
case .hdLegacyP2PKHWallet:
|
||||
return "walletHDLegacyP2PKH"
|
||||
case .hdLegacyBreadWallet:
|
||||
return "walletHDLegacyBread"
|
||||
case .aezeedWallet:
|
||||
return "walletAezeed"
|
||||
case .defaultGradients:
|
||||
return "walletLegacy"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ class WalletInformation: NSObject {
|
|||
@IBOutlet private weak var walletNameLabel: WKInterfaceLabel!
|
||||
@IBOutlet private weak var walletGroup: WKInterfaceGroup!
|
||||
static let identifier: String = "WalletInformation"
|
||||
let type: Wallet? = nil
|
||||
|
||||
var name: String = "" {
|
||||
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)
|
||||
name = wallet.label
|
||||
balance = wallet.hideBalance ? "" : wallet.balance
|
||||
type = WalletGradient(rawValue: wallet.type) ?? .SegwitHD
|
||||
}
|
||||
}
|
||||
|
|
198
ios/BlueWalletWatch Extension/Objects/WalletType.swift
Normal file
198
ios/BlueWalletWatch Extension/Objects/WalletType.swift
Normal 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
|
||||
}
|
||||
}
|
|
@ -1,125 +1,361 @@
|
|||
//
|
||||
// WatchDataSource.swift
|
||||
// BlueWalletWatch Extension
|
||||
//
|
||||
// Created by Marcos Rodriguez on 3/20/19.
|
||||
|
||||
//
|
||||
|
||||
// Data/WatchDataSource.swift
|
||||
|
||||
import Foundation
|
||||
import WatchConnectivity
|
||||
import KeychainSwift
|
||||
import Security
|
||||
import Combine
|
||||
import ClockKit
|
||||
|
||||
class WatchDataSource: NSObject {
|
||||
struct NotificationName {
|
||||
static let dataUpdated = Notification.Name(rawValue: "Notification.WalletDataSource.Updated")
|
||||
}
|
||||
struct Notifications {
|
||||
static let dataUpdated = Notification(name: NotificationName.dataUpdated)
|
||||
}
|
||||
|
||||
static let shared = WatchDataSource()
|
||||
var wallets: [Wallet] = [Wallet]()
|
||||
var companionWalletsInitialized = false
|
||||
private let keychain = KeychainSwift()
|
||||
let groupUserDefaults = UserDefaults(suiteName: UserDefaultsGroupKey.GroupName.rawValue)
|
||||
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
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()
|
||||
}
|
||||
}
|
||||
struct NotificationName {
|
||||
static let dataUpdated = Notification.Name(rawValue: "Notification.WalletDataSource.Updated")
|
||||
}
|
||||
struct Notifications {
|
||||
static let dataUpdated = Notification(name: NotificationName.dataUpdated)
|
||||
}
|
||||
|
||||
/// Represents the group user defaults keys.
|
||||
/// Ensure these match the keys used in your iOS app for sharing data.
|
||||
|
||||
/// Handles WatchConnectivity and data synchronization between iOS and Watch apps.
|
||||
class WatchDataSource: NSObject, ObservableObject, WCSessionDelegate {
|
||||
// MARK: - Singleton Instance
|
||||
|
||||
static func postDataUpdatedNotification() {
|
||||
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
|
||||
NotificationCenter.default.post(Notifications.dataUpdated)
|
||||
}
|
||||
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) {
|
||||
guard WatchDataSource.shared.wallets.count > walletIdentifier else {
|
||||
responseHandler("")
|
||||
return
|
||||
|
||||
// MARK: - Public Methods
|
||||
|
||||
/// Starts the WatchConnectivity session.
|
||||
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.
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func processData(data: [String: Any]) {
|
||||
|
||||
if let preferredFiatCurrency = data["preferredFiatCurrency"] as? String, let preferredFiatCurrencyUnit = fiatUnit(currency: preferredFiatCurrency) {
|
||||
groupUserDefaults?.set(preferredFiatCurrencyUnit.endPointKey, forKey: "preferredCurrency")
|
||||
groupUserDefaults?.synchronize()
|
||||
|
||||
// MARK: - Data Binding
|
||||
|
||||
/// 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)
|
||||
}
|
||||
|
||||
// MARK: - Keychain Operations
|
||||
|
||||
/// Loads wallets data from the Keychain asynchronously.
|
||||
private func loadKeychainData() {
|
||||
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
|
||||
}
|
||||
|
||||
// Filter wallets to include only on-chain wallets.
|
||||
let onChainWallets = decodedWallets.filter { $0.chain == .onchain }
|
||||
|
||||
DispatchQueue.main.async {
|
||||
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
|
||||
}
|
||||
|
||||
// Create an instance of ExtensionDelegate and call updatePreferredFiatCurrency()
|
||||
let extensionDelegate = ExtensionDelegate()
|
||||
extensionDelegate.updatePreferredFiatCurrency()
|
||||
var processedWallets: [Wallet] = []
|
||||
|
||||
} else if let isWalletsInitialized = data["isWalletsInitialized"] as? Bool {
|
||||
companionWalletsInitialized = isWalletsInitialized
|
||||
NotificationCenter.default.post(Notifications.dataUpdated)
|
||||
} else {
|
||||
WatchDataSource.shared.processWalletsData(walletsInfo: data)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -43,8 +43,8 @@ class ReceiveInterfaceController: WKInterfaceController {
|
|||
}
|
||||
|
||||
private func setupView() {
|
||||
if receiveMethod == .CreateInvoice && (wallet?.type == WalletGradient.LightningCustodial.rawValue) {
|
||||
presentController(withName: SpecifyInterfaceController.identifier, context: wallet?.identifier)
|
||||
if receiveMethod == .CreateInvoice && (wallet?.type == .lightningCustodianWallet) {
|
||||
presentController(withName: SpecifyInterfaceController.identifier, context: wallet?.id)
|
||||
} else {
|
||||
setupQRCode()
|
||||
setupMenuItems()
|
||||
|
@ -87,12 +87,12 @@ class ReceiveInterfaceController: WKInterfaceController {
|
|||
override func didAppear() {
|
||||
super.didAppear()
|
||||
if isCreatingInvoice() {
|
||||
presentController(withName: SpecifyInterfaceController.identifier, context: wallet?.identifier)
|
||||
presentController(withName: SpecifyInterfaceController.identifier, context: wallet?.id)
|
||||
}
|
||||
}
|
||||
|
||||
private func isCreatingInvoice() -> Bool {
|
||||
return receiveMethod == .CreateInvoice && (wallet?.type == WalletGradient.LightningCustodial.rawValue)
|
||||
return receiveMethod == .CreateInvoice && (wallet?.type == .lightningCustodianWallet)
|
||||
}
|
||||
|
||||
override func didDeactivate() {
|
||||
|
|
|
@ -40,7 +40,7 @@ class SpecifyInterfaceController: WKInterfaceController {
|
|||
let wallet = WatchDataSource.shared.wallets[identifier]
|
||||
self.wallet = wallet
|
||||
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
|
||||
guard let amountObject = notification.object as? [String], !amountObject.isEmpty else { return }
|
||||
if amountObject.count == 1 && (amountObject.first == "." || amountObject.first == "0") {
|
||||
|
@ -60,7 +60,7 @@ class SpecifyInterfaceController: WKInterfaceController {
|
|||
|
||||
var isShouldCreateButtonBeEnabled = amountDouble > 0 && !title.isEmpty
|
||||
|
||||
if (wallet.type == WalletGradient.LightningCustodial.rawValue) && !WCSession.default.isReachable {
|
||||
if (wallet.type == .lightningCustodianWallet) && !WCSession.default.isReachable {
|
||||
isShouldCreateButtonBeEnabled = false
|
||||
}
|
||||
|
||||
|
@ -89,14 +89,8 @@ class SpecifyInterfaceController: WKInterfaceController {
|
|||
}
|
||||
|
||||
@IBAction func createButtonTapped() {
|
||||
if WatchDataSource.shared.companionWalletsInitialized {
|
||||
NotificationCenter.default.post(name: NotificationName.createQRCode, object: specifiedQRContent)
|
||||
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()
|
||||
})])
|
||||
}
|
||||
NotificationCenter.default.post(name: NotificationName.createQRCode, object: specifiedQRContent)
|
||||
dismiss()
|
||||
}
|
||||
|
||||
override func contextForSegue(withIdentifier segueIdentifier: String) -> Any? {
|
||||
|
|
|
@ -21,15 +21,16 @@ class WalletDetailsInterfaceController: WKInterfaceController {
|
|||
|
||||
override func awake(withContext context: Any?) {
|
||||
super.awake(withContext: context)
|
||||
guard let identifier = context as? Int else {
|
||||
guard let identifier = context as? UUID else {
|
||||
pop()
|
||||
return
|
||||
}
|
||||
loadWalletDetails(identifier: identifier)
|
||||
}
|
||||
|
||||
private func loadWalletDetails(identifier: Int) {
|
||||
let wallet = WatchDataSource.shared.wallets[identifier]
|
||||
private func loadWalletDetails(identifier: UUID) {
|
||||
let index = WatchDataSource.shared.wallets.firstIndex(where: { $0.id == identifier }) ?? 0
|
||||
let wallet = WatchDataSource.shared.wallets[index]
|
||||
self.wallet = wallet
|
||||
updateWalletUI(wallet: wallet)
|
||||
updateTransactionsTable(forWallet: wallet)
|
||||
|
@ -39,16 +40,16 @@ class WalletDetailsInterfaceController: WKInterfaceController {
|
|||
walletBalanceLabel.setHidden(wallet.hideBalance)
|
||||
walletBalanceLabel.setText(wallet.hideBalance ? "" : wallet.balance)
|
||||
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)
|
||||
receiveButton.setHidden(wallet.receiveAddress.isEmpty)
|
||||
viewXPubButton.setHidden(!isXPubAvailable(wallet: wallet))
|
||||
}
|
||||
|
||||
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) {
|
||||
|
@ -77,8 +78,8 @@ class WalletDetailsInterfaceController: WKInterfaceController {
|
|||
}
|
||||
|
||||
@objc func showBalanceMenuItemTapped() {
|
||||
guard let identifier = wallet?.identifier else { return }
|
||||
WatchDataSource.toggleWalletHideBalance(walletIdentifier: identifier, hideBalance: false) { [weak self] _ in
|
||||
guard let identifier = wallet?.id else { return }
|
||||
WatchDataSource.shared.toggleWalletHideBalance(walletIdentifier: identifier, hideBalance: false) { [weak self] _ in
|
||||
DispatchQueue.main.async {
|
||||
WatchDataSource.postDataUpdatedNotification()
|
||||
self?.loadWalletDetails(identifier: identifier)
|
||||
|
@ -87,8 +88,8 @@ class WalletDetailsInterfaceController: WKInterfaceController {
|
|||
}
|
||||
|
||||
@objc func hideBalanceMenuItemTapped() {
|
||||
guard let identifier = wallet?.identifier else { return }
|
||||
WatchDataSource.toggleWalletHideBalance(walletIdentifier: identifier, hideBalance: true) { [weak self] _ in
|
||||
guard let identifier = wallet?.id else { return }
|
||||
WatchDataSource.shared.toggleWalletHideBalance(walletIdentifier: identifier, hideBalance: true) { [weak self] _ in
|
||||
DispatchQueue.main.async {
|
||||
WatchDataSource.postDataUpdatedNotification()
|
||||
self?.loadWalletDetails(identifier: identifier)
|
||||
|
@ -115,20 +116,13 @@ class WalletDetailsInterfaceController: WKInterfaceController {
|
|||
}
|
||||
|
||||
@IBAction func createInvoiceTapped() {
|
||||
if WatchDataSource.shared.companionWalletsInitialized {
|
||||
guard let wallet = wallet else { return }
|
||||
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()
|
||||
})])
|
||||
}
|
||||
guard let wallet = wallet else { return }
|
||||
pushController(withName: ReceiveInterfaceController.identifier, context: (wallet.id, ReceiveMethod.CreateInvoice))
|
||||
}
|
||||
|
||||
override func contextForSegue(withIdentifier segueIdentifier: String) -> Any? {
|
||||
guard let wallet = wallet else { return nil }
|
||||
return (wallet.identifier, ReceiveMethod.Onchain)
|
||||
return (wallet.id, ReceiveMethod.Onchain)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -2,19 +2,20 @@ import Foundation
|
|||
|
||||
class Balance {
|
||||
static func formatBalance(_ balance: Decimal, toUnit: BitcoinUnit, withFormatting: Bool = false, completion: @escaping (String) -> Void) {
|
||||
switch toUnit {
|
||||
case .BTC:
|
||||
let value = balance / Decimal(100_000_000)
|
||||
completion("\(value) BTC") // Localize unit names as needed.
|
||||
case .SATS:
|
||||
if withFormatting {
|
||||
completion(NumberFormatter.localizedString(from: balance as NSNumber, number: .decimal) + " SATS")
|
||||
} else {
|
||||
completion("\(balance) SATS")
|
||||
}
|
||||
case .LOCAL_CURRENCY:
|
||||
fetchLocalCurrencyEquivalent(satoshi: balance, completion: completion)
|
||||
switch toUnit {
|
||||
case .sats:
|
||||
if withFormatting {
|
||||
completion(NumberFormatter.localizedString(from: balance as NSNumber, number: .decimal) + " SATS")
|
||||
} else {
|
||||
completion("\(balance) SATS")
|
||||
}
|
||||
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) {
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,11 +5,52 @@
|
|||
// Created by Marcos Rodriguez on 4/14/24.
|
||||
// Copyright © 2024 BlueWallet. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum BitcoinUnit: String {
|
||||
case BTC = "BTC"
|
||||
case SATS = "SATS"
|
||||
case LOCAL_CURRENCY = "LOCAL_CURRENCY"
|
||||
/// Represents the various balance units used in the application.
|
||||
/// Conforms to `String`, `Codable`, `Equatable`, and `CustomStringConvertible` for easy encoding/decoding, comparisons, and descriptions.
|
||||
enum BitcoinUnit: String, Codable, Equatable, CustomStringConvertible {
|
||||
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
47
ios/Shared/Chain.swift
Normal 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
|
||||
}
|
||||
}
|
|
@ -11,6 +11,8 @@ import Foundation
|
|||
enum UserDefaultsGroupKey: String {
|
||||
case GroupName = "group.io.bluewallet.bluewallet"
|
||||
case PreferredCurrency = "preferredCurrency"
|
||||
case WatchAppBundleIdentifier = "io.bluewallet.bluewallet.watch"
|
||||
case BundleIdentifier = "io.bluewallet.bluewallet"
|
||||
case ElectrumSettingsHost = "electrum_host"
|
||||
case ElectrumSettingsTCPPort = "electrum_tcp_port"
|
||||
case ElectrumSettingsSSLPort = "electrum_ssl_port"
|
||||
|
|
70
ios/Shared/Utilities/KeychainHelper.swift
Normal file
70
ios/Shared/Utilities/KeychainHelper.swift
Normal 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
|
||||
}
|
||||
}
|
|
@ -19,9 +19,9 @@ struct WalletData {
|
|||
formatter.roundingMode = .up
|
||||
let value = NSNumber(value: balance / 100000000);
|
||||
if let valueString = formatter.string(from: value) {
|
||||
return "\(String(describing: valueString)) \(BitcoinUnit.BTC.rawValue)"
|
||||
return "\(String(describing: valueString)) \(BitcoinUnit.btc.rawValue)"
|
||||
} else {
|
||||
return "0 \(BitcoinUnit.BTC.rawValue)"
|
||||
return "0 \(BitcoinUnit.btc.rawValue)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue