diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9f4cca9631..34df1071ed 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -22,7 +22,7 @@ jobs: - name: Set up environment uses: ./.github/actions/setup - name: Build API - run: cd packages/loot-core && yarn build:api + run: cd packages/api && yarn build web: runs-on: ubuntu-latest diff --git a/packages/api/index.js b/packages/api/index.js index 6d163887e9..140f150600 100644 --- a/packages/api/index.js +++ b/packages/api/index.js @@ -4,14 +4,14 @@ let methods = require('./methods'); let utils = require('./utils'); let actualApp; -async function init({ budgetId, config } = {}) { +async function init(config = {}) { if (actualApp) { return; } global.fetch = require('node-fetch'); - await bundle.init({ budgetId, config }); + await bundle.init(config); actualApp = bundle.lib; injected.send = bundle.lib.send; @@ -30,5 +30,5 @@ module.exports = { shutdown, utils, internal: bundle.lib, - ...methods + ...methods, }; diff --git a/packages/api/methods.js b/packages/api/methods.js index 5caafa13d6..816f90bb58 100644 --- a/packages/api/methods.js +++ b/packages/api/methods.js @@ -20,6 +20,10 @@ async function loadBudget(budgetId) { return send('api/load-budget', { id: budgetId }); } +async function downloadBudget(syncId, { password } = {}) { + return send('api/download-budget', { syncId, password }); +} + async function batchBudgetUpdates(func) { await send('api/batch-budget-start'); try { @@ -89,7 +93,7 @@ function closeAccount(id, transferAccountId, transferCategoryId) { return send('api/account-close', { id, transferAccountId, - transferCategoryId + transferCategoryId, }); } @@ -172,6 +176,7 @@ module.exports = { q, loadBudget, + downloadBudget, batchBudgetUpdates, getBudgetMonths, getBudgetMonth, @@ -207,5 +212,5 @@ module.exports = { getPayeeRules, createPayeeRule, deletePayeeRule, - updatePayeeRule + updatePayeeRule, }; diff --git a/packages/api/package.json b/packages/api/package.json index 6e041bec08..e0f201d0f8 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -1,6 +1,6 @@ { "name": "@actual-app/api", - "version": "4.1.6", + "version": "5.0.0", "license": "MIT", "description": "An API for Actual", "main": "index.js", @@ -13,6 +13,9 @@ "migrations", "utils.js" ], + "scripts": { + "build": "yarn workspace loot-core build:api" + }, "dependencies": { "better-sqlite3": "^7.5.0", "node-fetch": "^2.6.9", diff --git a/packages/loot-core/src/client/actions/budgets.js b/packages/loot-core/src/client/actions/budgets.js index 548f86f141..295761727f 100644 --- a/packages/loot-core/src/client/actions/budgets.js +++ b/packages/loot-core/src/client/actions/budgets.js @@ -1,5 +1,5 @@ import { send } from '../../platform/client/fetch'; -import { getDownloadError } from '../../shared/errors'; +import { getDownloadError, getSyncError } from '../../shared/errors'; import constants from '../constants'; import { setAppState } from './app'; @@ -65,33 +65,25 @@ export function loadBudget(id, loadingText = '', options = {}) { let { error } = await send('load-budget', { id, ...options }); if (error) { + let message = getSyncError(error, id); if (error === 'out-of-sync-migrations' || error === 'out-of-sync-data') { // confirm is not available on iOS // eslint-disable-next-line if (typeof confirm !== 'undefined') { // eslint-disable-next-line let showBackups = confirm( - 'This budget cannot be loaded with this version of the app. ' + - 'Make sure the app is up-to-date. Do you want to load a backup?', + message + + ' Make sure the app is up-to-date. Do you want to load a backup?', ); if (showBackups) { dispatch(pushModal('load-backup', { budgetId: id })); } } else { - alert( - 'This budget cannot be loaded with this version of the app. ' + - 'Make sure the app is up-to-date.', - ); + alert(message + ' Make sure the app is up-to-date.'); } - } else if (error === 'budget-not-found') { - alert( - 'Budget file could not be found. If you changed something manually, please restart the app.', - ); } else { - alert( - 'Error loading budget. Please open a issue on GitHub for support.', - ); + alert(message); } dispatch(setAppState({ loadingText: null })); diff --git a/packages/loot-core/src/server/api.js b/packages/loot-core/src/server/api.js index 61f93e9b08..57c328a17b 100644 --- a/packages/loot-core/src/server/api.js +++ b/packages/loot-core/src/server/api.js @@ -1,3 +1,8 @@ +import { + getDownloadError, + getSyncError, + getTestKeyError, +} from '../shared/errors'; import * as monthUtils from '../shared/months'; import q from '../shared/query'; import { @@ -144,20 +149,54 @@ handlers['api/load-budget'] = async function ({ id }) { } else { connection.send('show-budgets'); - if (error === 'out-of-sync-migrations' || error === 'out-of-sync-data') { - throw new Error( - 'This budget cannot be loaded with this version of the app.', - ); - } else if (error === 'budget-not-found') { - throw new Error( - 'Budget "' + - id + - '" not found. Check the id of your budget in the "Advanced" section of the settings page.', - ); - } else { - throw new Error('We had an unknown problem opening "' + id + '".'); + throw new Error(getSyncError(error, id)); + } + } +}; + +handlers['api/download-budget'] = async function ({ syncId, password }) { + let { id: currentId } = prefs.getPrefs() || {}; + if (currentId) { + await handlers['close-budget'](); + } + + let localBudget = (await handlers['get-budgets']()).find( + b => b.groupId === syncId, + ); + if (localBudget) { + await handlers['load-budget']({ id: localBudget.id }); + let result = await handlers['sync-budget']({ id: localBudget.id }); + if (result.error) { + throw new Error(getSyncError(result.error, localBudget.id)); + } + } else { + let files = await handlers['get-remote-files'](); + let file = files.find(f => f.groupId === syncId); + if (!file) { + throw new Error( + `Budget "${syncId}" not found. Check the sync id of your budget in the "Advanced" section of the settings page.`, + ); + } + if (file.encryptKeyId && !password) { + throw new Error( + `File ${file.name} is encrypted. Please provide a password.`, + ); + } + if (password) { + let result = await handlers['key-test']({ + fileId: file.fileId, + password, + }); + if (result.error) { + throw new Error(getTestKeyError(result.error)); } } + + let result = await handlers['download-budget']({ fileId: file.fileId }); + if (result.error) { + throw new Error(getDownloadError(result.error, result.id)); + } + await handlers['load-budget']({ id: result.id }); } }; diff --git a/packages/loot-core/src/server/main.js b/packages/loot-core/src/server/main.js index 52a5a4c5cd..da668e0384 100644 --- a/packages/loot-core/src/server/main.js +++ b/packages/loot-core/src/server/main.js @@ -1635,19 +1635,21 @@ handlers['download-budget'] = async function ({ fileId, replace }) { } let id = result.id; - - // Load the budget and do a full sync - result = await loadBudget(result.id, VERSION, { showUpdate: true }); + await handlers['load-budget']({ id }); + result = await handlers['sync-budget']({ id }); + await handlers['close-budget'](); if (result.error) { - return { error: { reason: result.error } }; + return result; } + return { id }; +}; +// open and sync, but don’t close +handlers['sync-budget'] = async function ({ id }) { setSyncingMode('enabled'); await initialFullSync(); - await handlers['close-budget'](); - - return { id }; + return {}; }; handlers['load-budget'] = async function ({ id }) { @@ -2164,7 +2166,7 @@ export async function initApp(version, isDev, socketName) { } } -export async function init({ budgetId, config }) { +export async function init(config) { // Get from build // eslint-disable-next-line VERSION = ACTUAL_APP_VERSION; @@ -2184,6 +2186,12 @@ export async function init({ budgetId, config }) { if (serverURL) { setServer(serverURL); + + if (config.password) { + await runHandler(handlers['subscribe-sign-in'], { + password: config.password, + }); + } } else { // This turns off all server URLs. In this mode we don't want any // access to the server, we are doing things locally @@ -2194,10 +2202,6 @@ export async function init({ budgetId, config }) { }); } - if (budgetId) { - await runHandler(handlers['load-budget'], { id: budgetId }); - } - return lib; } diff --git a/packages/loot-core/src/shared/errors.js b/packages/loot-core/src/shared/errors.js index 88cd12e5cd..ff5cc85391 100644 --- a/packages/loot-core/src/shared/errors.js +++ b/packages/loot-core/src/shared/errors.js @@ -87,3 +87,17 @@ export function getSubscribeError({ reason }) { return 'An error occurred. Please try again later.'; } } + +export function getSyncError(error, id) { + if (error === 'out-of-sync-migrations' || error === 'out-of-sync-data') { + return 'This budget cannot be loaded with this version of the app.'; + } else if (error === 'budget-not-found') { + return ( + 'Budget "' + + id + + '" not found. Check the id of your budget in the "Advanced" section of the settings page.' + ); + } else { + return 'We had an unknown problem opening "' + id + '".'; + } +} diff --git a/packages/loot-core/webpack/webpack.api.config.js b/packages/loot-core/webpack/webpack.api.config.js index b7de754ada..5d90823b8d 100644 --- a/packages/loot-core/webpack/webpack.api.config.js +++ b/packages/loot-core/webpack/webpack.api.config.js @@ -1,14 +1,22 @@ +let path = require('path'); + let webpack = require('webpack'); + let config = require('./webpack.desktop.config'); config.resolve.extensions = ['.api.js', '.electron.js', '.js', '.json']; config.output.filename = 'bundle.api.js'; config.output.sourceMapFilename = 'bundle.api.js.map'; +config.output.path = path.join( + path.dirname(path.dirname(__dirname)), + 'api', + 'app', +); config.plugins.push( new webpack.DefinePlugin({ - ACTUAL_APP_VERSION: '"0.0.147"' - }) + ACTUAL_APP_VERSION: '"0.0.147"', + }), ); module.exports = config;