REF: old watchOS app

This commit is contained in:
Marcos Rodriguez Velez 2024-10-24 01:25:04 -04:00
parent cebdea6d25
commit 345565cd82
15 changed files with 617 additions and 788 deletions

View File

@ -1,246 +0,0 @@
import React, { useEffect, useRef } from 'react';
import {
transferCurrentComplicationUserInfo,
transferUserInfo,
updateApplicationContext,
useInstalled,
useReachability,
watchEvents,
} from 'react-native-watch-connectivity';
import Notifications from '../blue_modules/notifications';
import { MultisigHDWallet } from '../class';
import loc, { formatBalance, transactionTimeToReadable } from '../loc';
import { Chain } from '../models/bitcoinUnits';
import { FiatUnit } from '../models/fiatUnit';
import { useSettings } from '../hooks/context/useSettings';
import { useStorage } from '../hooks/context/useStorage';
function WatchConnectivity() {
const { walletsInitialized, wallets, fetchWalletTransactions, saveToDisk, txMetadata } = useStorage();
const { preferredFiatCurrency } = useSettings();
const isReachable = useReachability();
const isInstalled = useInstalled(); // true | false
const messagesListenerActive = useRef(false);
const lastPreferredCurrency = useRef(FiatUnit.USD.endPointKey);
useEffect(() => {
let messagesListener = () => {};
if (isInstalled && isReachable && walletsInitialized && messagesListenerActive.current === false) {
messagesListener = watchEvents.addListener('message', handleMessages);
messagesListenerActive.current = true;
} else {
messagesListener();
messagesListenerActive.current = false;
}
return () => {
messagesListener();
messagesListenerActive.current = false;
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [walletsInitialized, isReachable, isInstalled]);
useEffect(() => {
console.log(`Apple Watch: isInstalled: ${isInstalled}, isReachable: ${isReachable}, walletsInitialized: ${walletsInitialized}`);
if (isInstalled && walletsInitialized) {
constructWalletsToSendToWatch().then(walletsToProcess => {
if (walletsToProcess) {
if (isReachable) {
transferUserInfo(walletsToProcess);
console.log('Apple Watch: sent info to watch transferUserInfo');
} else {
updateApplicationContext(walletsToProcess);
console.log('Apple Watch: sent info to watch context');
}
}
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [walletsInitialized, wallets, isReachable, isInstalled]);
useEffect(() => {
if (walletsInitialized && isReachable && isInstalled) {
updateApplicationContext({ isWalletsInitialized: walletsInitialized, randomID: Math.floor(Math.random() * 11) });
}
}, [isInstalled, isReachable, walletsInitialized]);
useEffect(() => {
if (isInstalled && isReachable && walletsInitialized && preferredFiatCurrency) {
const preferredFiatCurrencyParsed = preferredFiatCurrency ?? FiatUnit.USD;
try {
if (lastPreferredCurrency.current !== preferredFiatCurrencyParsed.endPointKey) {
transferCurrentComplicationUserInfo({
preferredFiatCurrency: preferredFiatCurrencyParsed.endPointKey,
});
lastPreferredCurrency.current = preferredFiatCurrency.endPointKey;
} else {
console.log('WatchConnectivity lastPreferredCurrency has not changed');
}
} catch (e) {
console.log('WatchConnectivity useEffect preferredFiatCurrency error');
console.log(e);
}
}
}, [preferredFiatCurrency, walletsInitialized, isReachable, isInstalled]);
const handleMessages = (message, reply) => {
if (message.request === 'createInvoice') {
handleLightningInvoiceCreateRequest(message.walletIndex, message.amount, message.description)
.then(createInvoiceRequest => reply({ invoicePaymentRequest: createInvoiceRequest }))
.catch(e => {
console.log(e);
reply({});
});
} else if (message.message === 'sendApplicationContext') {
constructWalletsToSendToWatch().then(walletsToProcess => {
if (walletsToProcess) {
updateApplicationContext(walletsToProcess);
}
});
} else if (message.message === 'fetchTransactions') {
fetchWalletTransactions()
.then(() => saveToDisk())
.finally(() => reply({}));
} else if (message.message === 'hideBalance') {
const walletIndex = message.walletIndex;
const wallet = wallets[walletIndex];
wallet.hideBalance = message.hideBalance;
saveToDisk().finally(() => reply({}));
}
};
const handleLightningInvoiceCreateRequest = async (walletIndex, amount, description = loc.lnd.placeholder) => {
const wallet = wallets[walletIndex];
if (wallet.allowReceive() && amount > 0) {
try {
const invoiceRequest = await wallet.addInvoice(amount, description);
// lets decode payreq and subscribe groundcontrol so we can receive push notification when our invoice is paid
try {
// Let's verify if notifications are already configured. Otherwise the watch app will freeze waiting for user approval in iOS app
if (await Notifications.isNotificationsEnabled()) {
const decoded = await wallet.decodeInvoice(invoiceRequest);
Notifications.majorTomToGroundControl([], [decoded.payment_hash], []);
}
} catch (e) {
console.log('WatchConnectivity - Running in Simulator');
console.log(e);
}
return invoiceRequest;
} catch (error) {
return error;
}
}
};
const constructWalletsToSendToWatch = async () => {
if (!Array.isArray(wallets)) {
console.log('No Wallets set to sync with Watch app. Exiting...');
return;
}
if (!walletsInitialized) {
console.log('Wallets not initialized. Exiting...');
return;
}
const walletsToProcess = [];
for (const wallet of wallets) {
let receiveAddress;
if (wallet.chain === Chain.ONCHAIN) {
try {
receiveAddress = await wallet.getAddressAsync();
} catch (_) {}
if (!receiveAddress) {
// either sleep expired or getAddressAsync threw an exception
receiveAddress = wallet._getExternalAddressByIndex(wallet.next_free_address_index);
}
} else if (wallet.chain === Chain.OFFCHAIN) {
try {
await wallet.getAddressAsync();
receiveAddress = wallet.getAddress();
} catch (_) {}
if (!receiveAddress) {
// either sleep expired or getAddressAsync threw an exception
receiveAddress = wallet.getAddress();
}
}
const transactions = wallet.getTransactions(10);
const watchTransactions = [];
for (const transaction of transactions) {
let type = 'pendingConfirmation';
let memo = '';
let amount = 0;
if ('confirmations' in transaction && !(transaction.confirmations > 0)) {
type = 'pendingConfirmation';
} else if (transaction.type === 'user_invoice' || transaction.type === 'payment_request') {
const currentDate = new Date();
const now = (currentDate.getTime() / 1000) | 0; // eslint-disable-line no-bitwise
const invoiceExpiration = transaction.timestamp + transaction.expire_time;
if (invoiceExpiration > now) {
type = 'pendingConfirmation';
} else if (invoiceExpiration < now) {
if (transaction.ispaid) {
type = 'received';
} else {
type = 'sent';
}
}
} else if (transaction.value / 100000000 < 0) {
type = 'sent';
} else {
type = 'received';
}
if (transaction.type === 'user_invoice' || transaction.type === 'payment_request') {
amount = isNaN(transaction.value) ? '0' : amount;
const currentDate = new Date();
const now = (currentDate.getTime() / 1000) | 0; // eslint-disable-line no-bitwise
const invoiceExpiration = transaction.timestamp + transaction.expire_time;
if (invoiceExpiration > now) {
amount = formatBalance(transaction.value, wallet.getPreferredBalanceUnit(), true).toString();
} else if (invoiceExpiration < now) {
if (transaction.ispaid) {
amount = formatBalance(transaction.value, wallet.getPreferredBalanceUnit(), true).toString();
} else {
amount = loc.lnd.expired;
}
} else {
amount = formatBalance(transaction.value, wallet.getPreferredBalanceUnit(), true).toString();
}
} else {
amount = formatBalance(transaction.value, wallet.getPreferredBalanceUnit(), true).toString();
}
if (txMetadata[transaction.hash] && txMetadata[transaction.hash].memo) {
memo = txMetadata[transaction.hash].memo;
} else if (transaction.memo) {
memo = transaction.memo;
}
const watchTX = { type, amount, memo, time: transactionTimeToReadable(transaction.received) };
watchTransactions.push(watchTX);
}
const walletInformation = {
label: wallet.getLabel(),
balance: formatBalance(Number(wallet.getBalance()), wallet.getPreferredBalanceUnit(), true),
type: wallet.type,
preferredBalanceUnit: wallet.getPreferredBalanceUnit(),
receiveAddress,
transactions: watchTransactions,
hideBalance: wallet.hideBalance,
};
if (wallet.chain === Chain.ONCHAIN && wallet.type !== MultisigHDWallet.type) {
walletInformation.xpub = wallet.getXpub() ? wallet.getXpub() : wallet.getSecret();
}
if (wallet.allowBIP47() && wallet.isBIP47Enabled()) {
walletInformation.paymentCode = wallet.getBIP47PaymentCode();
}
walletsToProcess.push(walletInformation);
}
return { wallets: walletsToProcess, randomID: Math.floor(Math.random() * 11) };
};
return <></>;
}
export default WatchConnectivity;

View File

@ -0,0 +1,172 @@
import { useEffect, useRef, useCallback } from 'react';
import {
sendMessage,
sendMessageData,
startFileTransfer,
useReachability,
useInstalled,
watchEvents,
transferUserInfo,
updateApplicationContext,
transferCurrentComplicationUserInfo,
WatchMessage,
} from 'react-native-watch-connectivity';
import { useStorage } from '../hooks/context/useStorage';
import { useSettings } from '../hooks/context/useSettings';
import { LightningCustodianWallet } from '../class';
import { LightningTransaction, Transaction, TWallet } from '../class/wallets/types';
import { Chain } from '../models/bitcoinUnits';
const REQUEST_TYPES = {
CREATE_INVOICE: 'createInvoice',
FETCH_TRANSACTIONS: 'fetchTransactions',
HIDE_BALANCE: 'hideBalance',
};
interface WalletInfo {
label: string;
balance: number;
type: string;
preferredBalanceUnit: string;
receiveAddress: string;
transactions: Array<{
amount: number;
type: string;
memo: string;
time: number;
}>;
}
interface MessageRequest {
request: string;
walletIndex: number;
amount?: number;
description?: string;
hideBalance?: boolean;
}
export default function WatchConnectivity() {
const { walletsInitialized, wallets, fetchWalletTransactions, saveToDisk } = useStorage();
const { preferredFiatCurrency } = useSettings();
const isReachable = useReachability();
const isInstalled = useInstalled();
const messagesListenerActive = useRef(false);
useEffect(() => {
if (isInstalled && isReachable && walletsInitialized) {
syncWalletsToWatch();
}
if (!messagesListenerActive.current) {
const listener = watchEvents.addListener('message', handleMessages);
messagesListenerActive.current = true;
return () => {
listener.remove();
messagesListenerActive.current = false;
};
}
}, [isInstalled, isReachable, walletsInitialized]);
const syncWalletsToWatch = useCallback(() => {
const walletsToProcess: WalletInfo[] = wallets.map((wallet: TWallet | LightningCustodianWallet) => ({
label: wallet.getLabel(),
balance: wallet.getBalance(),
type: wallet.type,
preferredBalanceUnit: wallet.getPreferredBalanceUnit(),
receiveAddress: wallet.chain === Chain.ONCHAIN? wallet.getAddress() : wallet.getPaymentCode(),
transactions: wallet.getTransactions(10).map((transaction: Transaction & LightningTransaction) => ({
amount: transaction.value,
type: transaction.type,
memo: transaction.memo,
time: transaction.received,
})),
}));
const dataToSend = { wallets: walletsToProcess };
if (isReachable) {
transferUserInfo(dataToSend);
} else {
try {
updateApplicationContext(dataToSend);
} catch (error) {
console.error('Error updating application context:', error);
}
}
}, [wallets, isReachable]);
const handleMessages = useCallback(
async (message: WatchMessage<MessageRequest>, reply: (response: any) => void) => {
if (!isReachable) {
reply({});
return;
}
switch (message.request) {
case REQUEST_TYPES.CREATE_INVOICE: {
const invoice = await createLightningInvoice(message.walletIndex, message.amount || 0, message.description || '');
reply({ invoicePaymentRequest: invoice });
break;
}
case REQUEST_TYPES.FETCH_TRANSACTIONS: {
await fetchWalletTransactions();
await saveToDisk();
reply({});
break;
}
case REQUEST_TYPES.HIDE_BALANCE: {
handleHideBalance(message.walletIndex, message.hideBalance || false, reply);
break;
}
default: {
reply({});
break;
}
}
},
[wallets, isReachable]
);
const createLightningInvoice = async (walletIndex: number, amount: number, description: string) => {
const wallet = wallets[walletIndex] as LightningCustodianWallet;
if (!wallet || amount <= 0 || !wallet.allowReceive()) return '';
try {
const invoice = await wallet.addInvoice(amount, description || 'Invoice');
// @ts-ignore: fix later
if (await Notifications.isNotificationsEnabled()) {
const decoded = await wallet.decodeInvoice(invoice);
// @ts-ignore: fix later
Notifications.majorTomToGroundControl([], [decoded.payment_hash], []);
}
return invoice;
} catch (error) {
return '';
}
};
const handleHideBalance = (walletIndex: number, hideBalance: boolean, reply: (response: any) => void) => {
const wallet = wallets[walletIndex];
if (wallet) {
wallet.hideBalance = hideBalance;
saveToDisk().then(() => reply({}));
} else {
reply({});
}
};
const syncPreferredFiatCurrency = useCallback(() => {
if (preferredFiatCurrency) {
transferCurrentComplicationUserInfo({ preferredFiatCurrency: preferredFiatCurrency.endPointKey });
}
}, [preferredFiatCurrency]);
useEffect(() => {
if (isInstalled && preferredFiatCurrency) {
syncPreferredFiatCurrency();
}
}, [isInstalled, preferredFiatCurrency, syncPreferredFiatCurrency]);
return null;
}

View File

@ -39,27 +39,36 @@ const allWalletsBalanceAndTransactionTime = async (
if (!walletsInitialized || !(await isBalanceDisplayAllowed())) {
return { allWalletsBalance: 0, latestTransactionTime: 0 };
}
let balance = 0;
let latestTransactionTime: number | string = 0;
let latestConfirmedTransactionTime: number = 0;
let onlyUnconfirmedTransaction = true;
for (const wallet of wallets) {
if (wallet.hideBalance) continue;
balance += await wallet.getBalance();
const transactions: Transaction[] = await wallet.getTransactions();
for (const transaction of transactions) {
const transactionTime = await wallet.getLatestTransactionTimeEpoch();
if (transaction.confirmations > 0 && transactionTime > Number(latestTransactionTime)) {
latestTransactionTime = transactionTime;
if (transaction.confirmations > 0) {
// If a confirmed transaction exists, set onlyUnconfirmedTransaction to false
onlyUnconfirmedTransaction = false;
if (transactionTime > latestConfirmedTransactionTime) {
latestConfirmedTransactionTime = transactionTime;
}
}
}
if (latestTransactionTime === 0 && transactions[0]?.confirmations === 0) {
latestTransactionTime = WidgetCommunicationKeys.LatestTransactionIsUnconfirmed;
// If no confirmed transactions exist and there is exactly one unconfirmed transaction
if (onlyUnconfirmedTransaction && transactions.length === 1 && transactions[0]?.confirmations === 0) {
return { allWalletsBalance: balance, latestTransactionTime: WidgetCommunicationKeys.LatestTransactionIsUnconfirmed };
}
}
return { allWalletsBalance: balance, latestTransactionTime };
return { allWalletsBalance: balance, latestTransactionTime: latestConfirmedTransactionTime || 0 };
};
const WidgetCommunication: React.FC = () => {

View File

@ -120,9 +120,11 @@
B45010A62C1507DE00619044 /* CustomSegmentedControlManager.m in Sources */ = {isa = PBXBuildFile; fileRef = B45010A52C1507DE00619044 /* CustomSegmentedControlManager.m */; };
B4549F362B82B10D002E3153 /* ci_post_clone.sh in Resources */ = {isa = PBXBuildFile; fileRef = B4549F352B82B10D002E3153 /* ci_post_clone.sh */; };
B461B852299599F800E431AA /* AppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = B461B851299599F800E431AA /* AppDelegate.mm */; };
B48A6A292C1DF01000030AB9 /* KeychainSwift in Frameworks */ = {isa = PBXBuildFile; productRef = B48A6A282C1DF01000030AB9 /* KeychainSwift */; };
B4AB225D2B02AD12001F4328 /* XMLParserDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4AB225C2B02AD12001F4328 /* XMLParserDelegate.swift */; };
B4AB225E2B02AD12001F4328 /* XMLParserDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4AB225C2B02AD12001F4328 /* XMLParserDelegate.swift */; };
B4AB64512CC9F50200D55A9D /* KeychainManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4AB64502CC9F50200D55A9D /* KeychainManager.swift */; };
B4AB64522CC9F50300D55A9D /* KeychainManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4AB64502CC9F50200D55A9D /* KeychainManager.swift */; };
B4AB64532CC9F50300D55A9D /* KeychainManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4AB64502CC9F50200D55A9D /* KeychainManager.swift */; };
B4B1A4622BFA73110072E3BB /* WidgetHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4B1A4612BFA73110072E3BB /* WidgetHelper.swift */; };
B4B1A4642BFA73110072E3BB /* WidgetHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4B1A4612BFA73110072E3BB /* WidgetHelper.swift */; };
B4D0B2622C1DEA11006B6B1B /* ReceivePageInterfaceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4D0B2612C1DEA11006B6B1B /* ReceivePageInterfaceController.swift */; };
@ -131,7 +133,7 @@
B4D0B2682C1DED67006B6B1B /* ReceiveMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4D0B2672C1DED67006B6B1B /* ReceiveMethod.swift */; };
B4EE583C226703320003363C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B40D4E35225841ED00428FCC /* Assets.xcassets */; };
B4EFF73B2C3F6C5E0095D655 /* MockData.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4EFF73A2C3F6C5E0095D655 /* MockData.swift */; };
C978A716948AB7DEC5B6F677 /* BuildFile in Frameworks */ = {isa = PBXBuildFile; };
C978A716948AB7DEC5B6F677 /* (null) in Frameworks */ = {isa = PBXBuildFile; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@ -350,6 +352,7 @@
B47B21EB2B2128B8001F6690 /* BlueWalletUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlueWalletUITests.swift; sourceTree = "<group>"; };
B49038D82B8FBAD300A8164A /* BlueWalletUITest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlueWalletUITest.swift; sourceTree = "<group>"; };
B4AB225C2B02AD12001F4328 /* XMLParserDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = XMLParserDelegate.swift; sourceTree = "<group>"; };
B4AB64502CC9F50200D55A9D /* KeychainManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainManager.swift; sourceTree = "<group>"; };
B4B1A4612BFA73110072E3BB /* WidgetHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetHelper.swift; sourceTree = "<group>"; };
B4B31A352C77BBA000663334 /* nb */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nb; path = nb.lproj/Interface.strings; sourceTree = "<group>"; };
B4D0B2612C1DEA11006B6B1B /* ReceivePageInterfaceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReceivePageInterfaceController.swift; sourceTree = "<group>"; };
@ -381,7 +384,7 @@
files = (
782F075B5DD048449E2DECE9 /* libz.tbd in Frameworks */,
764B49B1420D4AEB8109BF62 /* libsqlite3.0.tbd in Frameworks */,
C978A716948AB7DEC5B6F677 /* BuildFile in Frameworks */,
C978A716948AB7DEC5B6F677 /* (null) in Frameworks */,
17CDA0718F42DB2CE856C872 /* libPods-BlueWallet.a in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -415,7 +418,6 @@
files = (
B41B76852B66B2FF002C48D5 /* Bugsnag in Frameworks */,
B41B76872B66B2FF002C48D5 /* BugsnagNetworkRequestPlugin in Frameworks */,
B48A6A292C1DF01000030AB9 /* KeychainSwift in Frameworks */,
6DFC807024EA0B6C007B8700 /* EFQRCode in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -732,6 +734,7 @@
B450109A2C0FCD7E00619044 /* Utilities */ = {
isa = PBXGroup;
children = (
B4AB64502CC9F50200D55A9D /* KeychainManager.swift */,
B450109B2C0FCD8A00619044 /* Utilities.swift */,
);
path = Utilities;
@ -881,7 +884,6 @@
6DFC806F24EA0B6C007B8700 /* EFQRCode */,
B41B76842B66B2FF002C48D5 /* Bugsnag */,
B41B76862B66B2FF002C48D5 /* BugsnagNetworkRequestPlugin */,
B48A6A282C1DF01000030AB9 /* KeychainSwift */,
);
productName = "BlueWalletWatch Extension";
productReference = B40D4E3C225841ED00428FCC /* BlueWalletWatch Extension.appex */;
@ -962,7 +964,6 @@
packageReferences = (
6DFC806E24EA0B6C007B8700 /* XCRemoteSwiftPackageReference "EFQRCode" */,
B41B76832B66B2FF002C48D5 /* XCRemoteSwiftPackageReference "bugsnag-cocoa" */,
B48A6A272C1DF01000030AB9 /* XCRemoteSwiftPackageReference "keychain-swift" */,
);
productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */;
projectDirPath = "";
@ -1177,6 +1178,7 @@
B461B852299599F800E431AA /* AppDelegate.mm in Sources */,
B44033F42BCC377F00162242 /* WidgetData.swift in Sources */,
B44033C42BCC332400162242 /* Balance.swift in Sources */,
B4AB64512CC9F50200D55A9D /* KeychainManager.swift in Sources */,
B44034072BCC38A000162242 /* FiatUnit.swift in Sources */,
B44034002BCC37F800162242 /* Bundle+decode.swift in Sources */,
B44033E22BCC36CB00162242 /* Placeholders.swift in Sources */,
@ -1219,6 +1221,7 @@
6DD410BA266CAF5C0087DE03 /* FiatUnit.swift in Sources */,
B44033FB2BCC379200162242 /* WidgetDataStore.swift in Sources */,
B44033EB2BCC371A00162242 /* MarketData.swift in Sources */,
B4AB64522CC9F50300D55A9D /* KeychainManager.swift in Sources */,
6DD410AF266CAF5C0087DE03 /* WalletInformationAndMarketWidget.swift in Sources */,
B44033C62BCC332400162242 /* Balance.swift in Sources */,
B44033E62BCC36FF00162242 /* WalletData.swift in Sources */,
@ -1264,6 +1267,7 @@
B40D4E632258425500428FCC /* ReceiveInterfaceController.swift in Sources */,
B43D0378225847C500FBAA95 /* WalletGradient.swift in Sources */,
B4EFF73B2C3F6C5E0095D655 /* MockData.swift in Sources */,
B4AB64532CC9F50300D55A9D /* KeychainManager.swift in Sources */,
B44033C02BCC32F800162242 /* BitcoinUnit.swift in Sources */,
B44033E52BCC36FF00162242 /* WalletData.swift in Sources */,
B44033EF2BCC374500162242 /* Numeric+abbreviated.swift in Sources */,
@ -2052,14 +2056,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 */
@ -2078,11 +2074,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 */;

View File

@ -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",

View File

@ -1,17 +1,16 @@
//
// ComplicationController.swift
// T WatchKit Extension
// BlueWalletWatch Extension
//
// Created by Marcos Rodriguez on 8/24/19.
// Copyright © 2019 Marcos Rodriguez. All rights reserved.
//
import ClockKit
class ComplicationController: NSObject, CLKComplicationDataSource {
private let groupUserDefaults = UserDefaults(suiteName: UserDefaultsGroupKey.GroupName.rawValue)
// MARK: - Timeline Configuration
func getSupportedTimeTravelDirections(for complication: CLKComplication, withHandler handler: @escaping (CLKComplicationTimeTravelDirections) -> Void) {
@ -22,12 +21,12 @@ class ComplicationController: NSObject, CLKComplicationDataSource {
handler(nil)
}
@available(watchOSApplicationExtension 7.0, *)
func complicationDescriptors() async -> [CLKComplicationDescriptor] {
return [CLKComplicationDescriptor(
identifier: "io.bluewallet.bluewallet",
displayName: "Market Price",
supportedFamilies: CLKComplicationFamily.allCases)]
supportedFamilies: CLKComplicationFamily.allCases
)]
}
func getTimelineEndDate(for complication: CLKComplication, withHandler handler: @escaping (Date?) -> Void) {
@ -42,269 +41,107 @@ class ComplicationController: NSObject, CLKComplicationDataSource {
func getCurrentTimelineEntry(
for complication: CLKComplication,
withHandler handler: @escaping (CLKComplicationTimelineEntry?) -> Void)
{
let marketData: WidgetDataStore? = groupUserDefaults?.codable(forKey: MarketData.string)
withHandler handler: @escaping (CLKComplicationTimelineEntry?) -> Void
) {
// Load market data from user defaults
guard let marketData: WidgetDataStore = groupUserDefaults?.codable(forKey: MarketData.string) else {
handler(nil)
return
}
let date = marketData.date ?? Date()
let valueLabel = marketData.formattedRateForComplication ?? "--"
let valueSmallLabel = marketData.formattedRateForSmallComplication ?? "--"
let currencySymbol = groupUserDefaults?.string(forKey: "preferredCurrency")
.flatMap { fiatUnit(currency: $0)?.symbol } ?? fiatUnit(currency: "USD")!.symbol
let timeLabel = marketData.formattedDate ?? "--"
let entry: CLKComplicationTimelineEntry
let date: Date
let valueLabel: String
let valueSmallLabel: String
let currencySymbol: String
let timeLabel: String
if let price = marketData?.formattedRateForComplication, let priceAbbreviated = marketData?.formattedRateForSmallComplication, let marketDatadata = marketData?.date, let lastUpdated = marketData?.formattedDate {
date = marketDatadata
valueLabel = price
timeLabel = lastUpdated
valueSmallLabel = priceAbbreviated
if let preferredFiatCurrency = groupUserDefaults?.string(forKey: "preferredCurrency"), let preferredFiatUnit = fiatUnit(currency: preferredFiatCurrency) {
currencySymbol = preferredFiatUnit.symbol
} else {
currencySymbol = fiatUnit(currency: "USD")!.symbol
}
} else {
valueLabel = "--"
timeLabel = "--"
valueSmallLabel = "--"
currencySymbol = fiatUnit(currency: "USD")!.symbol
date = Date()
}
let line2Text = CLKSimpleTextProvider(text:currencySymbol)
let line1SmallText = CLKSimpleTextProvider(text: valueSmallLabel)
// Handle different complication families
switch complication.family {
case .circularSmall:
let template = CLKComplicationTemplateCircularSmallStackText()
template.line1TextProvider = line1SmallText
template.line2TextProvider = line2Text
let template = CLKComplicationTemplateCircularSmallStackText(
line1TextProvider: CLKSimpleTextProvider(text: valueSmallLabel),
line2TextProvider: CLKSimpleTextProvider(text: currencySymbol)
)
entry = CLKComplicationTimelineEntry(date: date, complicationTemplate: template)
handler(entry)
case .utilitarianSmallFlat:
let template = CLKComplicationTemplateUtilitarianSmallFlat()
if #available(watchOSApplicationExtension 6.0, *) {
template.textProvider = CLKTextProvider(format: "%@%@", currencySymbol, valueSmallLabel)
} else {
handler(nil)
}
let template = CLKComplicationTemplateUtilitarianSmallFlat(
textProvider: CLKTextProvider(format: "%@%@", currencySymbol, valueSmallLabel)
)
entry = CLKComplicationTimelineEntry(date: date, complicationTemplate: template)
handler(entry)
case .utilitarianSmall:
let template = CLKComplicationTemplateUtilitarianSmallRingImage()
template.imageProvider = CLKImageProvider(onePieceImage: UIImage(named: "Complication/Utilitarian")!)
entry = CLKComplicationTimelineEntry(date: date, complicationTemplate: template)
handler(entry)
case .graphicCircular:
if #available(watchOSApplicationExtension 6.0, *) {
let template = CLKComplicationTemplateGraphicCircularStackText()
template.line1TextProvider = line1SmallText
template.line2TextProvider = line2Text
let template = CLKComplicationTemplateGraphicCircularStackText(
line1TextProvider: CLKSimpleTextProvider(text: valueSmallLabel),
line2TextProvider: CLKSimpleTextProvider(text: currencySymbol)
)
entry = CLKComplicationTimelineEntry(date: date, complicationTemplate: template)
handler(entry)
} else {
handler(nil)
}
case .modularSmall:
let template = CLKComplicationTemplateModularSmallStackText()
template.line1TextProvider = line1SmallText
template.line2TextProvider = line2Text
let template = CLKComplicationTemplateModularSmallStackText(
line1TextProvider: CLKSimpleTextProvider(text: valueSmallLabel),
line2TextProvider: CLKSimpleTextProvider(text: currencySymbol)
)
entry = CLKComplicationTimelineEntry(date: date, complicationTemplate: template)
handler(entry)
case .graphicCorner:
let template = CLKComplicationTemplateGraphicCornerStackText()
if #available(watchOSApplicationExtension 6.0, *) {
template.outerTextProvider = CLKTextProvider(format: "%@", valueSmallLabel)
template.innerTextProvider = CLKTextProvider(format: "%@", currencySymbol)
} else {
handler(nil)
}
let template = CLKComplicationTemplateGraphicCornerStackText(
innerTextProvider: CLKSimpleTextProvider(text: valueSmallLabel), // Inner text first
outerTextProvider: CLKSimpleTextProvider(text: currencySymbol) // Outer text second
)
entry = CLKComplicationTimelineEntry(date: date, complicationTemplate: template)
handler(entry)
case .graphicBezel:
let template = CLKComplicationTemplateGraphicBezelCircularText()
if #available(watchOSApplicationExtension 6.0, *) {
template.textProvider = CLKTextProvider(format: "%@%@", currencySymbol, valueSmallLabel)
let imageProvider = CLKFullColorImageProvider(fullColorImage: UIImage(named: "Complication/Graphic Bezel")!)
let circularTemplate = CLKComplicationTemplateGraphicCircularImage()
circularTemplate.imageProvider = imageProvider
template.circularTemplate = circularTemplate
entry = CLKComplicationTimelineEntry(date: date, complicationTemplate: template)
handler(entry)
} else {
default:
handler(nil)
}
case .utilitarianLarge:
if #available(watchOSApplicationExtension 7.0, *) {
let textProvider = CLKTextProvider(format: "%@%@", currencySymbol, valueLabel)
let template = CLKComplicationTemplateUtilitarianLargeFlat(textProvider: textProvider)
entry = CLKComplicationTimelineEntry(date: date, complicationTemplate: template)
handler(entry)
} else {
handler(nil)
}
case .modularLarge:
let template = CLKComplicationTemplateModularLargeStandardBody()
if #available(watchOSApplicationExtension 6.0, *) {
template.headerTextProvider = CLKTextProvider(format: "Bitcoin Price")
template.body1TextProvider = CLKTextProvider(format: "%@%@", currencySymbol, valueLabel)
template.body2TextProvider = CLKTextProvider(format: "at %@", timeLabel)
entry = CLKComplicationTimelineEntry(date: date, complicationTemplate: template)
handler(entry)
} else {
handler(nil)
}
case .extraLarge:
let template = CLKComplicationTemplateExtraLargeStackText()
if #available(watchOSApplicationExtension 6.0, *) {
template.line1TextProvider = CLKTextProvider(format: "%@%@", currencySymbol, valueLabel)
template.line2TextProvider = CLKTextProvider(format: "at %@", timeLabel)
entry = CLKComplicationTimelineEntry(date: date, complicationTemplate: template)
handler(entry)
} else {
handler(nil)
}
case .graphicRectangular:
let template = CLKComplicationTemplateGraphicRectangularStandardBody()
if #available(watchOSApplicationExtension 6.0, *) {
template.headerTextProvider = CLKTextProvider(format: "Bitcoin Price")
template.body1TextProvider = CLKTextProvider(format: "%@%@", currencySymbol, valueLabel)
template.body2TextProvider = CLKTextProvider(format: "at %@", timeLabel)
entry = CLKComplicationTimelineEntry(date: date, complicationTemplate: template)
handler(entry)
} else {
handler(nil)
}
case .graphicExtraLarge:
if #available(watchOSApplicationExtension 7.0, *) {
let template = CLKComplicationTemplateGraphicExtraLargeCircularStackText()
template.line1TextProvider = CLKTextProvider(format: "%@%@", currencySymbol, valueLabel)
template.line1TextProvider = CLKTextProvider(format: "at %@", timeLabel)
entry = CLKComplicationTimelineEntry(date: date, complicationTemplate: template)
handler(entry)
} else {
handler(nil)
}
@unknown default:
fatalError()
}
return
}
handler(entry)
}
// MARK: - Timeline Entries
func getTimelineEntries(for complication: CLKComplication, before date: Date, limit: Int, withHandler handler: @escaping ([CLKComplicationTimelineEntry]?) -> Void) {
// Call the handler with the timeline entries prior to the given date
handler(nil)
}
func getTimelineEntries(for complication: CLKComplication, after date: Date, limit: Int, withHandler handler: @escaping ([CLKComplicationTimelineEntry]?) -> Void) {
// Call the handler with the timeline entries after to the given date
handler(nil)
}
// MARK: - Placeholder Templates
func getLocalizableSampleTemplate(for complication: CLKComplication, withHandler handler: @escaping (CLKComplicationTemplate?) -> Void) {
// This method will be called once per supported complication, and the results will be cached
let line1Text = CLKSimpleTextProvider(text: "46 K")
let line2Text = CLKSimpleTextProvider(text: "$")
let lineTimeText = CLKSimpleTextProvider(text:"3:40 PM")
// Provide sample template for different complication families
switch complication.family {
case .circularSmall:
let template = CLKComplicationTemplateCircularSmallStackText()
template.line1TextProvider = line1Text
template.line2TextProvider = line2Text
let template = CLKComplicationTemplateCircularSmallStackText(
line1TextProvider: line1Text,
line2TextProvider: line2Text
)
handler(template)
case .utilitarianSmallFlat:
let template = CLKComplicationTemplateUtilitarianSmallFlat()
if #available(watchOSApplicationExtension 6.0, *) {
template.textProvider = CLKTextProvider(format: "%@", "$46,134")
} else {
handler(nil)
}
handler(template)
case .utilitarianSmall:
let template = CLKComplicationTemplateUtilitarianSmallRingImage()
template.imageProvider = CLKImageProvider(onePieceImage: UIImage(named: "Complication/Utilitarian")!)
handler(template)
case .graphicCircular:
if #available(watchOSApplicationExtension 6.0, *) {
let template = CLKComplicationTemplateGraphicCircularStackText()
template.line1TextProvider = line1Text
template.line2TextProvider = line2Text
handler(template)
} else {
handler(nil)
}
case .graphicCorner:
let template = CLKComplicationTemplateGraphicCornerStackText()
if #available(watchOSApplicationExtension 6.0, *) {
template.outerTextProvider = CLKTextProvider(format: "46,134")
template.innerTextProvider = CLKTextProvider(format: "$")
} else {
handler(nil)
}
let template = CLKComplicationTemplateUtilitarianSmallFlat(
textProvider: CLKTextProvider(format: "$46,134")
)
handler(template)
case .modularSmall:
let template = CLKComplicationTemplateModularSmallStackText()
template.line1TextProvider = line1Text
template.line2TextProvider = line2Text
let template = CLKComplicationTemplateModularSmallStackText(
line1TextProvider: line1Text,
line2TextProvider: line2Text
)
handler(template)
case .utilitarianLarge:
if #available(watchOSApplicationExtension 7.0, *) {
let textProvider = CLKTextProvider(format: "%@%@", "$", "46,000")
let template = CLKComplicationTemplateUtilitarianLargeFlat(textProvider: textProvider)
handler(template)
} else {
default:
handler(nil)
}
case .graphicBezel:
let template = CLKComplicationTemplateGraphicBezelCircularText()
if #available(watchOSApplicationExtension 6.0, *) {
template.textProvider = CLKTextProvider(format: "%@%@", "$S", "46,000")
let imageProvider = CLKFullColorImageProvider(fullColorImage: UIImage(named: "Complication/Graphic Bezel")!)
let circularTemplate = CLKComplicationTemplateGraphicCircularImage()
circularTemplate.imageProvider = imageProvider
template.circularTemplate = circularTemplate
handler(template)
} else {
handler(nil)
}
case .modularLarge:
let template = CLKComplicationTemplateModularLargeStandardBody()
if #available(watchOSApplicationExtension 6.0, *) {
template.headerTextProvider = CLKTextProvider(format: "Bitcoin Price")
template.body1TextProvider = CLKTextProvider(format: "%@%@", "$S", "46,000")
template.body2TextProvider = lineTimeText
handler(template)
} else {
handler(nil)
}
case .extraLarge:
let template = CLKComplicationTemplateExtraLargeStackText()
template.line1TextProvider = line1Text
template.line2TextProvider = lineTimeText
handler(template)
case .graphicRectangular:
let template = CLKComplicationTemplateGraphicRectangularStandardBody()
if #available(watchOSApplicationExtension 6.0, *) {
template.headerTextProvider = CLKTextProvider(format: "Bitcoin Price")
template.body1TextProvider = CLKTextProvider(format: "%@%@", "$S", "46,000")
template.body2TextProvider = CLKTextProvider(format: "%@", Date().description)
handler(template)
} else {
handler(nil)
}
case .graphicExtraLarge:
if #available(watchOSApplicationExtension 7.0, *) {
let template = CLKComplicationTemplateGraphicExtraLargeCircularStackText()
template.line1TextProvider = line1Text
template.line2TextProvider = line2Text
handler(template)
} else {
handler(nil)
}
@unknown default:
fatalError()
}
}

View File

@ -3,7 +3,6 @@
// BlueWalletWatch Extension
//
// Created by Marcos Rodriguez on 3/6/19.
//
import WatchKit
@ -17,6 +16,8 @@ class ExtensionDelegate: NSObject, WKExtensionDelegate {
func applicationDidFinishLaunching() {
scheduleNextReload()
updatePreferredFiatCurrency()
// Initialize Bugsnag based on user preference
if let isDoNotTrackEnabled = groupUserDefaults?.bool(forKey: "donottrack"), !isDoNotTrackEnabled {
Bugsnag.start()
}
@ -28,7 +29,9 @@ class ExtensionDelegate: NSObject, WKExtensionDelegate {
}
private func fetchPreferredFiatUnit() -> FiatUnit? {
if let preferredFiatCurrency = groupUserDefaults?.string(forKey: "preferredCurrency"), let preferredFiatUnit = fiatUnit(currency: preferredFiatCurrency) {
// Fetch the preferred fiat currency unit from user defaults, default to USD
if let preferredFiatCurrency = groupUserDefaults?.string(forKey: "preferredCurrency"),
let preferredFiatUnit = fiatUnit(currency: preferredFiatCurrency) {
return preferredFiatUnit
} else {
return fiatUnit(currency: "USD")
@ -37,10 +40,17 @@ class ExtensionDelegate: NSObject, WKExtensionDelegate {
private func updateMarketData(for fiatUnit: FiatUnit) {
MarketAPI.fetchPrice(currency: fiatUnit.endPointKey) { (data, error) in
guard let data = data, let encodedData = try? PropertyListEncoder().encode(data) else { return }
guard let data = data, let encodedData = try? PropertyListEncoder().encode(data) else {
print("Failed to fetch market data or encode it: \(String(describing: error))")
return
}
// Store the market data in user defaults
let groupUserDefaults = UserDefaults(suiteName: UserDefaultsGroupKey.GroupName.rawValue)
groupUserDefaults?.set(encodedData, forKey: MarketData.string)
groupUserDefaults?.synchronize()
// Reload complications to reflect the updated market data
ExtensionDelegate.reloadActiveComplications()
}
}
@ -53,6 +63,7 @@ class ExtensionDelegate: NSObject, WKExtensionDelegate {
}
func nextReloadTime(after date: Date) -> Date {
// Calculate the next reload time (every 10 minutes)
let calendar = Calendar(identifier: .gregorian)
return calendar.date(byAdding: .minute, value: 10, to: date)!
}
@ -86,5 +97,4 @@ class ExtensionDelegate: NSObject, WKExtensionDelegate {
updateMarketData(for: fiatUnitUserDefaults)
backgroundTask.setTaskCompletedWithSnapshot(false)
}
}

View File

@ -30,11 +30,6 @@ class InterfaceController: WKInterfaceController, WCSessionDelegate {
WCSession.default.activate()
}
private func processContextData(_ context: Any?) {
guard let contextUnwrapped = context as? [String: Any] else { return }
WatchDataSource.shared.processData(data: contextUnwrapped)
}
@objc private func updateUI() {
let wallets = WatchDataSource.shared.wallets
let isEmpty = wallets.isEmpty
@ -59,23 +54,26 @@ class InterfaceController: WKInterfaceController, WCSessionDelegate {
}
func session(_ session: WCSession, didReceiveApplicationContext applicationContext: [String : Any]) {
print("Received application context data:", applicationContext)
WatchDataSource.shared.processData(data: applicationContext)
}
func session(_ session: WCSession, didReceiveUserInfo userInfo: [String : Any] = [:]) {
print("Received user info:", userInfo)
WatchDataSource.shared.processData(data: userInfo)
}
func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
if activationState == .activated {
WatchDataSource.shared.loadKeychainData()
print("Watch session activated successfully.")
WatchDataSource.shared.loadWalletsData()
} else if let error = error {
print("Watch session activation failed with error: \(error)")
}
}
func session(_ session: WCSession, didReceiveMessage message: [String : Any]) {
print("Received message:", message)
WatchDataSource.shared.processData(data: message)
}
}

View File

@ -1,22 +1,17 @@
//
// Wallet.swift
// Transaction.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"
struct Transaction: Codable {
let time: String
let memo: String
let amount: String
let type: String
let amount: String
init(time: String, memo: String, type: String, amount: String) {
self.time = time
@ -24,18 +19,4 @@ class Transaction: NSObject, NSSecureCoding {
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
}
}

View File

@ -3,18 +3,12 @@
// 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?
struct Wallet: Codable {
let identifier: String
let label: String
let balance: String
let type: String
@ -25,7 +19,7 @@ class Wallet: NSObject, NSSecureCoding {
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?) {
init(label: String, balance: String, type: String, preferredBalanceUnit: String, receiveAddress: String, transactions: [Transaction], identifier: String, xpub: String?, hideBalance: Bool = false, paymentCode: String?) {
self.label = label
self.balance = balance
self.type = type
@ -37,30 +31,4 @@ class Wallet: NSObject, NSSecureCoding {
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
}
}

View File

@ -3,67 +3,106 @@
// BlueWalletWatch Extension
//
// Created by Marcos Rodriguez on 3/20/19.
//
import Foundation
import WatchConnectivity
import KeychainSwift
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)
static let groupUserDefaults = UserDefaults(suiteName: UserDefaultsGroupKey.GroupName.rawValue)
// Use a constant for the keychain identifier
private static let walletKeychainIdentifier = "WalletKeychainData"
override init() {
super.init()
loadKeychainData()
loadWalletsData()
}
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 }
// Load wallet data from Keychain or other secure storage using Codable
func loadWalletsData() {
if let existingData = KeychainManager.shared.getData(forKey: WatchDataSource.walletKeychainIdentifier) { // Use a static key
do {
let walletData = try JSONDecoder().decode([Wallet].self, from: existingData)
wallets = walletData
WatchDataSource.postDataUpdatedNotification()
} catch {
print("Failed to decode wallets from Keychain: \(error)")
}
} else {
print("No data found in Keychain")
}
}
// Process wallet data received from the iPhone app via WatchConnectivity
func processWalletsData(walletsInfo: [String: Any]) {
if let walletsToProcess = walletsInfo["wallets"] as? [[String: Any]] {
wallets.removeAll();
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 {
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]] else {
print("Invalid wallet data")
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 }
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 {
print("Invalid transaction data")
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)
let paymentCode = entry["paymentCode"] as? String ?? ""
let wallet = Wallet(
label: label,
balance: balance,
type: type,
preferredBalanceUnit: preferredBalanceUnit,
receiveAddress: receiveAddress,
transactions: transactionsProcessed,
identifier: String(index), // Use index as the identifier here
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)
}
// Save the updated wallets back to Keychain using Codable
do {
let encodedWallets = try JSONEncoder().encode(wallets)
KeychainManager.shared.set(encodedWallets, forKey: WatchDataSource.walletKeychainIdentifier) // Use the static key
WatchDataSource.postDataUpdatedNotification()
} catch {
print("Failed to encode and save wallets: \(error)")
}
} else {
print("Invalid wallets data received")
}
}
@ -71,55 +110,66 @@ class WatchDataSource: NSObject {
NotificationCenter.default.post(Notifications.dataUpdated)
}
// Request a Lightning invoice from the iPhone companion app
static func requestLightningInvoice(walletIdentifier: Int, amount: Double, description: String?, responseHandler: @escaping (_ invoice: String) -> Void) {
guard WatchDataSource.shared.wallets.count > walletIdentifier else {
responseHandler("")
return
}
WCSession.default.sendMessage(["request": "createInvoice", "walletIndex": walletIdentifier, "amount": amount, "description": description ?? ""], replyHandler: { (reply: [String : Any]) in
print("Requesting Lightning invoice from companion app for wallet index \(walletIdentifier) with amount \(amount)")
WCSession.default.sendMessage(
["request": "createInvoice", "walletIndex": String(walletIdentifier), "amount": amount, "description": description ?? ""], // Convert walletIdentifier to String
replyHandler: { (reply: [String: Any]) in
if let invoicePaymentRequest = reply["invoicePaymentRequest"] as? String, !invoicePaymentRequest.isEmpty {
print("Received Lightning invoice: \(invoicePaymentRequest)")
responseHandler(invoicePaymentRequest)
} else {
print("Invalid invoice received or empty response")
responseHandler("")
}
}) { (error) in
print(error)
},
errorHandler: { (error) in
print("Error requesting Lightning invoice: \(error)")
responseHandler("")
}
)
}
static func toggleWalletHideBalance(walletIdentifier: Int, hideBalance: Bool, responseHandler: @escaping (_ invoice: String) -> Void) {
guard WatchDataSource.shared.wallets.count > walletIdentifier else {
// Toggle the wallet hide balance option and send a message to the iPhone companion app
static func toggleWalletHideBalance(walletIdentifier: String, hideBalance: Bool, responseHandler: @escaping (_ result: String) -> Void) {
guard WatchDataSource.shared.wallets.count > Int(walletIdentifier)! else {
responseHandler("")
return
}
WCSession.default.sendMessage(["message": "hideBalance", "walletIndex": walletIdentifier, "hideBalance": hideBalance], replyHandler: { (reply: [String : Any]) in
responseHandler("")
}) { (error) in
print(error)
responseHandler("")
print("Toggling hide balance for wallet index \(walletIdentifier) to \(hideBalance)")
WCSession.default.sendMessage(
["message": "hideBalance", "walletIndex": String(walletIdentifier), "hideBalance": hideBalance], // Convert walletIdentifier to String
replyHandler: { _ in
print("Successfully toggled hide balance")
responseHandler("")
},
errorHandler: { (error) in
print("Error toggling hide balance: \(error)")
responseHandler("")
}
)
}
// Process the data received from the iPhone companion app, including fiat currency and wallet data
func processData(data: [String: Any]) {
if let preferredFiatCurrency = data["preferredFiatCurrency"] as? String,
let preferredFiatCurrencyUnit = fiatUnit(currency: preferredFiatCurrency) {
WatchDataSource.groupUserDefaults?.set(preferredFiatCurrencyUnit.endPointKey, forKey: "preferredCurrency")
WatchDataSource.groupUserDefaults?.synchronize()
if let preferredFiatCurrency = data["preferredFiatCurrency"] as? String, let preferredFiatCurrencyUnit = fiatUnit(currency: preferredFiatCurrency) {
groupUserDefaults?.set(preferredFiatCurrencyUnit.endPointKey, forKey: "preferredCurrency")
groupUserDefaults?.synchronize()
// Create an instance of ExtensionDelegate and call updatePreferredFiatCurrency()
let extensionDelegate = ExtensionDelegate()
extensionDelegate.updatePreferredFiatCurrency()
} else if let isWalletsInitialized = data["isWalletsInitialized"] as? Bool {
companionWalletsInitialized = isWalletsInitialized
NotificationCenter.default.post(Notifications.dataUpdated)
print("Updated preferred fiat currency to \(preferredFiatCurrency)")
} else {
WatchDataSource.shared.processWalletsData(walletsInfo: data)
}
}
}

View File

@ -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()
})])
}
}
override func contextForSegue(withIdentifier segueIdentifier: String) -> Any? {

View File

@ -21,15 +21,15 @@ 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? String else {
pop()
return
}
loadWalletDetails(identifier: identifier)
}
private func loadWalletDetails(identifier: Int) {
let wallet = WatchDataSource.shared.wallets[identifier]
private func loadWalletDetails(identifier: String) {
let wallet = WatchDataSource.shared.wallets[Int(identifier)!]
self.wallet = wallet
updateWalletUI(wallet: wallet)
updateTransactionsTable(forWallet: wallet)
@ -78,7 +78,7 @@ class WalletDetailsInterfaceController: WKInterfaceController {
@objc func showBalanceMenuItemTapped() {
guard let identifier = wallet?.identifier else { return }
WatchDataSource.toggleWalletHideBalance(walletIdentifier: identifier, hideBalance: false) { [weak self] _ in
WatchDataSource.toggleWalletHideBalance(walletIdentifier: String(identifier), hideBalance: false) { [weak self] _ in
DispatchQueue.main.async {
WatchDataSource.postDataUpdatedNotification()
self?.loadWalletDetails(identifier: identifier)
@ -88,7 +88,7 @@ class WalletDetailsInterfaceController: WKInterfaceController {
@objc func hideBalanceMenuItemTapped() {
guard let identifier = wallet?.identifier else { return }
WatchDataSource.toggleWalletHideBalance(walletIdentifier: identifier, hideBalance: true) { [weak self] _ in
WatchDataSource.toggleWalletHideBalance(walletIdentifier: String(identifier), hideBalance: true) { [weak self] _ in
DispatchQueue.main.async {
WatchDataSource.postDataUpdatedNotification()
self?.loadWalletDetails(identifier: identifier)
@ -115,15 +115,8 @@ 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()
})])
}
}
override func contextForSegue(withIdentifier segueIdentifier: String) -> Any? {

View File

@ -0,0 +1,77 @@
//
// KeychainManager.swift
// BlueWallet
//
// Created by Marcos Rodriguez on 10/23/24.
// Copyright © 2024 BlueWallet. All rights reserved.
//
import Foundation
import Security
class KeychainManager {
static let shared = KeychainManager()
private init() {}
// MARK: - Save Data to Keychain
func set(_ data: Data, forKey key: String) -> Bool {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
kSecValueData as String: data
]
// Remove any existing item before adding new one
SecItemDelete(query as CFDictionary)
let status = SecItemAdd(query as CFDictionary, nil)
return status == errSecSuccess
}
// MARK: - Get Data from Keychain
func getData(forKey key: String) -> Data? {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
kSecReturnData as String: true,
kSecMatchLimit as String: kSecMatchLimitOne
]
var result: AnyObject?
let status = SecItemCopyMatching(query as CFDictionary, &result)
if status == errSecSuccess, let data = result as? Data {
return data
}
return nil
}
// MARK: - Remove Data from Keychain
func delete(forKey key: String) -> Bool {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key
]
let status = SecItemDelete(query as CFDictionary)
return status == errSecSuccess
}
// MARK: - Update Data in Keychain
func update(_ data: Data, forKey key: String) -> Bool {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key
]
let attributesToUpdate: [String: Any] = [
kSecValueData as String: data
]
let status = SecItemUpdate(query as CFDictionary, attributesToUpdate as CFDictionary)
return status == errSecSuccess
}
}

View File

@ -21,14 +21,15 @@ struct WalletInformationView: View {
let amount = numberFormatter.string(from: NSNumber(value: ((allWalletsBalance.balance / 100000000) * marketData.rate))) ?? ""
return amount
}
var formattedLatestTransactionTime: String {
var formattedLatestTransactionTime: String? {
if allWalletsBalance.latestTransactionTime.isUnconfirmed == true {
return "Pending..."
} else if allWalletsBalance.latestTransactionTime.epochValue == 0 {
return "Never"
return nil
}
guard let epochValue = allWalletsBalance.latestTransactionTime.epochValue else {
return "Never"
return nil
}
let forDate = Date(timeIntervalSince1970: TimeInterval(epochValue / 1000))
let dateFormatter = RelativeDateTimeFormatter()
@ -41,14 +42,17 @@ struct WalletInformationView: View {
VStack(alignment: .leading, spacing: nil, content: {
Text(allWalletsBalance.formattedBalanceBTC).font(Font.system(size: 15, weight: .medium, design: .default)).foregroundColor(.textColorLightGray).lineLimit(1).minimumScaleFactor(0.01)
Text(formattedBalance).lineLimit(1).foregroundColor(.textColor).font(Font.system(size: 28, weight: .bold, design: .default)).minimumScaleFactor(0.01)
Spacer()
// Conditionally render latest transaction time if it's valid
if let latestTransaction = formattedLatestTransactionTime {
Text("Latest transaction").font(Font.system(size: 11, weight: .regular, design: .default)).foregroundColor(.textColorLightGray)
Text(formattedLatestTransactionTime).lineLimit(1).foregroundColor(.textColor).font(Font.system(size:13, weight: .regular, design: .default)).minimumScaleFactor(0.01)
}).frame(minWidth: 0,
Text(latestTransaction).lineLimit(1).foregroundColor(.textColor).font(Font.system(size: 13, weight: .regular, design: .default)).minimumScaleFactor(0.01)
}
})
.frame(minWidth: 0,
maxWidth: .infinity,
minHeight: 0,
maxHeight: .infinity,