Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Standalone POS #1722

Merged
merged 17 commits into from
Dec 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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 @@ -52,6 +52,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 @@ -293,6 +297,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 @@ -217,8 +217,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 @@ -499,8 +500,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 @@ -694,14 +697,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 @@ -860,8 +880,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 @@ -958,4 +981,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;
};
}