Skip to content

Commit

Permalink
Merge pull request #1722 from Talej/pos
Browse files Browse the repository at this point in the history
Standalone POS
  • Loading branch information
kaloudis committed Dec 28, 2023
2 parents 37411c6 + d054810 commit 3c5ba98
Show file tree
Hide file tree
Showing 23 changed files with 2,910 additions and 332 deletions.
1 change: 1 addition & 0 deletions App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export default class App extends React.PureComponent {
MessageSignStore={Stores.messageSignStore}
ActivityStore={Stores.activityStore}
PosStore={Stores.posStore}
InventoryStore={Stores.inventoryStore}
ModalStore={Stores.modalStore}
NotesStore={Stores.notesStore}
SyncStore={Stores.syncStore}
Expand Down
16 changes: 16 additions & 0 deletions Navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ import Mortals from './views/Settings/Mortals';
import PointOfSale from './views/Settings/PointOfSale';
import PointOfSaleRecon from './views/Settings/PointOfSaleRecon';
import PointOfSaleReconExport from './views/Settings/PointOfSaleReconExport';
import Categories from './views/POS/Categories';
import ProductCategoryDetails from './views/POS/ProductCategoryDetails';
import Products from './views/POS/Products';
import ProductDetails from './views/POS/ProductDetails';
import PaymentsSettings from './views/Settings/PaymentsSettings';
import InvoicesSettings from './views/Settings/InvoicesSettings';
import LSP from './views/Settings/LSP';
Expand Down Expand Up @@ -294,6 +298,18 @@ const AppScenes = {
PointOfSaleReconExport: {
screen: PointOfSaleReconExport
},
Categories: {
screen: Categories
},
ProductCategoryDetails: {
screen: ProductCategoryDetails
},
Products: {
screen: Products
},
ProductDetails: {
screen: ProductDetails
},
PaymentsSettings: {
screen: PaymentsSettings
},
Expand Down
15 changes: 10 additions & 5 deletions components/WalletHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import Clipboard from '@react-native-clipboard/clipboard';

import ChannelsStore from '../stores/ChannelsStore';
import LightningAddressStore from '../stores/LightningAddressStore';
import SettingsStore from '../stores/SettingsStore';
import SettingsStore, { PosEnabled } from '../stores/SettingsStore';
import NodeInfoStore from '../stores/NodeInfoStore';
import PosStore from '../stores/PosStore';
import SyncStore from '../stores/SyncStore';
Expand Down Expand Up @@ -251,8 +251,9 @@ export default class WalletHeader extends React.Component<
settings.nodes[settings.selectedNode || 0]) ||
null;

const squareEnabled: boolean =
(settings && settings.pos && settings.pos.squareEnabled) || false;
const posEnabled: PosEnabled =
(settings && settings.pos && settings.pos.posEnabled) ||
PosEnabled.Disabled;

const SettingsButton = () => (
<TouchableOpacity
Expand Down Expand Up @@ -533,8 +534,12 @@ export default class WalletHeader extends React.Component<
<View>
<ScanBadge navigation={navigation} />
</View>
{squareEnabled && (
<View style={{ marginLeft: 15 }}>
{posEnabled !== PosEnabled.Disabled && (
<View
style={{
marginLeft: 15
}}
>
<POSBadge
setPosStatus={setPosStatus}
getOrders={getOrders}
Expand Down
25 changes: 24 additions & 1 deletion locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
"general.pay": "Pay",
"general.open": "Open",
"general.settled": "Settled",
"general.new": "New",
"general.loading": "Loading",
"general.conversionRate": "Conversion rate",
"general.bitcoin": "Bitcoin",
Expand All @@ -75,6 +76,8 @@
"general.noneSelected": "None selected",
"general.zeusDefaults": "Using Zeus defaults",
"general.restartZeusChanges": "Restart Zeus for changes to take effect",
"general.charge": "Charge",
"general.clear": "Clear",
"general.lightningAddress": "Lightning address",
"general.lightningAddressCondensed": "LN Address",
"general.good": "Good",
Expand Down Expand Up @@ -701,14 +704,31 @@
"views.Settings.Help.telegram": "Telegram (we will not DM you)",
"views.Settings.Help.email": "Email support",
"views.Settings.POS.enableSquare": "Enable Square POS integration",
"views.Settings.POS.enablePos": "Enable POS integration",
"views.Settings.POS.squareAccessToken": "Square Access token",
"views.Settings.POS.squareLocationId": "Square Location ID",
"views.Settings.POS.merchantName": "Merchant name (Optional, used for invoice memos)",
"views.Settings.POS.confPref": "Confirmation preference",
"views.Settings.POS.disableTips": "Disable tips",
"views.Settings.POS.taxPercentage": "Tax percentage",
"views.Settings.POS.devMode": "Developer mode",
"views.Settings.POS.recon": "Reconciliation",
"views.Settings.POS.reconExport": "Reconciliation Export",
"views.Settings.POS.Categories": "Categories",
"views.Settings.POS.Category": "Product category",
"views.Settings.POS.Category.name": "Category",
"views.Settings.POS.saveCategory": "Save category",
"views.Settings.POS.Products": "Products",
"views.Settings.POS.Product": "Product",
"views.Settings.POS.Product.name": "Product name",
"views.Settings.POS.Product.sku": "SKU",
"views.Settings.POS.Product.pricedIn": "Priced in",
"views.Settings.POS.Product.fiat": "Fiat",
"views.Settings.POS.Product.sats": "sats",
"views.Settings.POS.Product.price": "Price",
"views.Settings.POS.Product.active": "Active",
"views.Settings.POS.saveProduct": "Save product",
"views.Settings.POS.confirmDelete": "Confirm delete",
"views.Settings.Seed.title": "Back up wallet",
"views.Settings.Seed.text1": "The following 24 words are your wallet backup.",
"views.Settings.Seed.text2": "KEEP THEM SAFE as anyone who sees these words can steal your funds.",
Expand Down Expand Up @@ -869,8 +889,11 @@
"error.failureReasonIncorrectPaymentDetails": "Payment details incorrect (unknown hash, invalid amt or invalid final cltv delta)",
"error.failureReasonInsufficientBalance": "Insufficient local balance",
"pos.views.Wallet.PosPane.noOrders": "No orders open at the moment. To send to ZEUS, mark order as 'Other Payment Type' with a note that includes 'Zeus', 'BTC', or 'Bitcoin'",
"pos.views.Wallet.PosPane.noOrdersStandalone": "No orders open at the moment",
"pos.views.Wallet.PosPane.noOrdersPaid": "No orders have been paid yet",
"pos.views.Wallet.PosPane.fetchingRates": "Fetching exchange rates",
"pos.views.Wallet.PosPane.noProducts": "No products have been created yet",
"pos.views.Wallet.PosPane.uncategorized": "Uncategorized",
"pos.views.Order.tax": "Tax",
"pos.views.Order.subtotalBitcoin": "Subtotal (Bitcoin)",
"pos.views.Order.subtotalFiat": "Subtotal (fiat)",
Expand Down Expand Up @@ -968,4 +991,4 @@
"views.LspExplanationOverview.buttonText": "Learn more about wrapped invoices",
"views.Sweep.title": "Sweep on-chain wallet",
"views.Sweep.explainer": "Sweeping the on-chain wallet will send both the confirmed and unconfirmed balance to the destination address specified above."
}
}
8 changes: 7 additions & 1 deletion models/Order.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,14 @@ import BaseModel from './BaseModel';
import { localeString } from './../utils/LocaleUtils';
import { orderPaymentInfo } from './../stores/PosStore';

interface LineItem {
interface BasePriceMoney {
amount: number;
sats?: number;
}
export interface LineItem {
name: string;
quantity: number;
base_price_money: BasePriceMoney;
}

export default class Order extends BaseModel {
Expand All @@ -21,6 +26,7 @@ export default class Order extends BaseModel {
total_money: {
amount: number;
currency: string;
sats?: number;
};
line_items: Array<LineItem>;
payment?: orderPaymentInfo;
Expand Down
26 changes: 26 additions & 0 deletions models/Product.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { observable, computed } from 'mobx';
import BaseModel from './BaseModel';

export enum PricedIn {
Sats = 'sats',
Fiat = 'fiat'
}

export enum ProductStatus {
Active = 'active',
Inactive = 'inactive'
}

export default class Product extends BaseModel {
@observable public id: string;
@observable public name: string;
@observable public sku: string;
@observable public pricedIn: PricedIn;
@observable public price: number;
@observable public category: string;
@observable public status: ProductStatus;

@computed public get model(): string {
return 'Product';
}
}
11 changes: 11 additions & 0 deletions models/ProductCategory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { observable, computed } from 'mobx';
import BaseModel from './BaseModel';

export default class ProductCategory extends BaseModel {
@observable public id: string;
@observable public name: string;

@computed public get model(): string {
return 'ProductCategory';
}
}
134 changes: 134 additions & 0 deletions stores/InventoryStore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import { action, observable } from 'mobx';
import Product from '../models/Product';
import ProductCategory from '../models/ProductCategory';
import EncryptedStorage from 'react-native-encrypted-storage';

const CATEGORY_KEY = 'zeus-product-categories';
const PRODUCT_KEY = 'zeus-products';

export default class InventoryStore {
@observable categories: Array<ProductCategory> = [];
@observable products: Array<Product> = [];
@observable public loading = false;

@action
public async getInventory() {
this.loading = true;
try {
// Retrieve the categories
const categories = await EncryptedStorage.getItem(CATEGORY_KEY);
if (categories) {
this.categories = JSON.parse(categories) || [];
}
// Retrieve the products
const products = await EncryptedStorage.getItem(PRODUCT_KEY);
if (products) {
this.products = JSON.parse(products) || [];
}
} catch (error) {
console.error('Could not load inventory', error);
} finally {
this.loading = false;
}

return {
categories: this.categories,
products: this.products
};
}

@action
public async setCategories(categories: string) {
this.loading = true;
await EncryptedStorage.setItem(CATEGORY_KEY, categories);
this.loading = false;
return categories;
}

@action
public updateCategories = async (newCategory: ProductCategory) => {
const { categories: existingCategories } = await this.getInventory();

const found = existingCategories.find((c) => c.id === newCategory.id);
if (found) {
const { products } = await this.getInventory();
const updatedProducts = products
.filter((product) => product.category === found.name)
.map(
(product) =>
({ ...product, category: newCategory.name } as Product)
);
if (updatedProducts.length > 0) {
await this.updateProducts(updatedProducts);
}

found.name = newCategory.name;
} else {
existingCategories.push(newCategory);
}

await this.setCategories(JSON.stringify(existingCategories));

const { categories } = await this.getInventory();
return categories;
};

@action
public deleteCategory = async (categoryId: string) => {
const { categories: existingCategories } = await this.getInventory();

const idx = existingCategories.findIndex((c) => c.id === categoryId);
if (idx > -1) {
existingCategories.splice(idx, 1);
await this.setCategories(JSON.stringify(existingCategories));

const { categories } = await this.getInventory();
return categories;
}
};

@action
public async setProducts(products: string) {
this.loading = true;
await EncryptedStorage.setItem(PRODUCT_KEY, products);
this.loading = false;
return products;
}

@action
public updateProducts = async (newProducts: Product[]) => {
const { products: existingProducts } = await this.getInventory();

newProducts.forEach((newProduct) => {
const found = existingProducts.find((c) => c.id === newProduct.id);
if (found) {
found.name = newProduct.name;
found.sku = newProduct.sku;
found.price = newProduct.price;
found.pricedIn = newProduct.pricedIn;
found.category = newProduct.category;
found.status = newProduct.status;
} else {
existingProducts.push(newProduct);
}
});

await this.setProducts(JSON.stringify(existingProducts));
// ensure we get the enhanced settings set
const { products } = await this.getInventory();
return products;
};

@action
public deleteProduct = async (productIds: string[]) => {
const { products: existingProducts } = await this.getInventory();

const updatedProducts = existingProducts.filter(
(p) => !productIds.includes(p.id)
);
await this.setProducts(JSON.stringify(updatedProducts));

const { products } = await this.getInventory();
return products;
};
}

0 comments on commit 3c5ba98

Please sign in to comment.