Skip to content

Commit

Permalink
Merge pull request #183 from SFTtech/milo/app-fixes
Browse files Browse the repository at this point in the history
app fixes
  • Loading branch information
mikonse authored Jan 2, 2024
2 parents 2ddfaf9 + b52fee4 commit aa0af67
Show file tree
Hide file tree
Showing 60 changed files with 3,928 additions and 8,103 deletions.
36 changes: 22 additions & 14 deletions .github/workflows/frontend.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -108,20 +108,28 @@ jobs:
- name: Setup Gradle
uses: gradle/gradle-build-action@v2

- name: Write gradle.properties to include signing key configuration for android release build
if: ${{ !failure() && !cancelled() && startsWith(github.ref, 'refs/heads/master')}}
env:
KEYSTORE: ${{ secrets.ANDROID_KEYSTORE_B64 }}
KEYSTORE_ALIAS: ${{ secrets.ANDROID_KEY_STORE_ALIAS }}
KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEY_STORE_PASSWORD }}
KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
run: |
echo "${KEYSTORE}" | base64 --decode > abrechnung-app-upload.keystore
ls -lah .
mkdir -p ~/.gradle
echo "ABRECHNUNG_UPLOAD_STORE_FILE=$(pwd)/abrechnung-app-upload.keystore" >> ~/.gradle/gradle.properties
echo "ABRECHNUNG_UPLOAD_KEY_ALIAS=${KEYSTORE_ALIAS}" >> ~/.gradle/gradle.properties
echo "ABRECHNUNG_UPLOAD_STORE_PASSWORD=${KEYSTORE_PASSWORD}" >> ~/.gradle/gradle.properties
echo "ABRECHNUNG_UPLOAD_KEY_PASSWORD=${KEY_PASSWORD}" >> ~/.gradle/gradle.properties
cat ~/.gradle/gradle.properties
- name: Build App APK
run: npx nx build-android mobile --tasks assembleRelease

# - name: Sign App APK
# id: sign_app_apk
# uses: r0adkll/sign-android-release@v1
# with:
# releaseDirectory: frontend/apps/mobile/android/app/build/outputs/apk/release
# signingKeyBase64: ${{ secrets.ANDROID_SIGNING_KEY }}
# alias: ${{ secrets.ANDROID_KEY_STORE_ALIAS }}
# keyStorePassword: ${{ secrets.ANDROID_KEY_STORE_PASSWORD }}

# - name: Upload APK
# uses: actions/upload-artifact@v3
# with:
# name: app-release-apk
# path: ${{steps.sign_app_apk.outputs.signedReleaseFile}}
- name: Upload APK
uses: actions/upload-artifact@v3
with:
name: app-release-apk
path: frontend/apps/mobile/android/app/build/outputs/apk/release/app-release.apk
2 changes: 1 addition & 1 deletion .github/workflows/push_on_master.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ jobs:
with:
files: |
debs/*.deb
frontend/apps/mobile/android/app/build/outputs/apk/debug/app-debug.apk
frontend/apps/mobile/android/app/build/outputs/apk/release/app-release.apk
# if it's not already published, keep the release as a draft.
draft: true
# mark it as a prerelease if the tag contains 'rc'.
Expand Down
8 changes: 8 additions & 0 deletions abrechnung/application/groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,14 @@ async def join_group(self, *, conn: Connection, user: User, invite_token: str) -
if not group:
raise PermissionError(f"Invalid invite token")

user_is_already_member = await conn.fetchval(
"select exists (select user_id from group_membership where user_id = $1 and group_id = $2)",
user.id,
invite["group_id"],
)
if user_is_already_member:
raise InvalidCommand(f"User is already a member of this group")

await conn.execute(
"insert into group_membership (user_id, group_id, invited_by, can_write, is_owner) "
"values ($1, $2, $3, $4, false)",
Expand Down
1 change: 0 additions & 1 deletion abrechnung/application/transactions.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
from abrechnung.core.errors import InvalidCommand, NotFoundError
from abrechnung.core.service import Service
from abrechnung.domain.transactions import (
ALLOWED_FILETYPES,
NewFile,
NewTransaction,
NewTransactionPosition,
Expand Down
37 changes: 21 additions & 16 deletions abrechnung/application/users.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from datetime import datetime, timezone
from typing import Optional

import asyncpg
from asyncpg.pool import Pool
from email_validator import EmailNotValidError, validate_email
from jose import JWTError, jwt
Expand Down Expand Up @@ -31,6 +30,20 @@ class TokenMetadata(BaseModel):
session_id: int


async def _check_user_exists(*, conn: Connection, username: str, email: str):
user_exists = await conn.fetchrow(
"select "
"exists(select from usr where username = $1) as username_exists, "
"exists(select from usr where email = $2) as email_exists",
username,
email,
)
if user_exists["username_exists"]:
raise InvalidCommand("A user with this username already exists")
if user_exists["email_exists"]:
raise InvalidCommand("A user with this email already exists")


class UserService(Service):
def __init__(
self,
Expand Down Expand Up @@ -96,18 +109,6 @@ async def _verify_user_password(self, user_id: int, password: str) -> bool:

return self._check_password(password, user["hashed_password"])

@with_db_transaction
async def is_session_token_valid(self, *, conn: Connection, token: str) -> Optional[tuple[int, int]]:
"""returns the session id"""
row = await conn.fetchrow(
"select user_id, id from session where token = $1 and valid_until is null or valid_until > now()",
token,
)
if row:
await conn.execute("update session set last_seen = now() where token = $1", token)

return row

@with_db_transaction
async def login_user(
self, *, conn: Connection, username: str, password: str, session_name: str
Expand Down Expand Up @@ -154,9 +155,10 @@ async def logout_user(self, *, conn: Connection, user: User, session_id: int):

@with_db_transaction
async def demo_register_user(self, *, conn: Connection, username: str, email: str, password: str) -> int:
await _check_user_exists(conn=conn, username=username, email=email)
hashed_password = self._hash_password(password)
user_id = await conn.fetchval(
"insert into usr (username, email, hashed_password, pending) " "values ($1, $2, $3, false) returning id",
"insert into usr (username, email, hashed_password, pending) values ($1, $2, $3, false) returning id",
username,
email,
hashed_password,
Expand All @@ -166,7 +168,8 @@ async def demo_register_user(self, *, conn: Connection, username: str, email: st

return user_id

def _validate_email_address(self, email: str) -> str:
@staticmethod
def _validate_email_address(email: str) -> str:
try:
valid = validate_email(email)
email = valid.email
Expand Down Expand Up @@ -200,14 +203,16 @@ async def register_user(
if not self.enable_registration:
raise PermissionError(f"User registrations are disabled on this server")

await _check_user_exists(conn=conn, username=username, email=email)

email = self._validate_email_address(email)

is_guest_user = False
has_valid_email = self._validate_email_domain(email)

if invite_token is not None and self.allow_guest_users and not has_valid_email:
invite = await conn.fetchval(
"select id " "from group_invite where token = $1 and valid_until > now()",
"select id from group_invite where token = $1 and valid_until > now()",
invite_token,
)
if invite is None:
Expand Down
4 changes: 3 additions & 1 deletion frontend/.eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@
{
"files": ["*.ts", "*.tsx"],
"extends": ["plugin:@nx/typescript"],
"rules": {}
"rules": {
"@typescript-eslint/no-explicit-any": ["warn"]
}
},
{
"files": ["*.js", "*.jsx"],
Expand Down
14 changes: 2 additions & 12 deletions frontend/apps/mobile/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,23 +76,13 @@
},
"lint": {
"executor": "@nx/eslint:lint",
"outputs": ["{options.outputFile}"],
"options": {
"lintFilePatterns": ["apps/mobile/**/*.{ts,tsx,js,jsx}"]
}
"outputs": ["{options.outputFile}"]
},
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "apps/mobile/jest.config.ts",
"passWithNoTests": true
},
"configurations": {
"ci": {
"ci": true,
"codeCoverage": true
}
"jestConfig": "apps/mobile/jest.config.ts"
}
}
},
Expand Down
7 changes: 7 additions & 0 deletions frontend/apps/mobile/src/@types/debug.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export type DebugExpand<T> = T extends infer O ? { [K in keyof O]: O[K] } : never;

export type DebugExpandRecursive<T> = T extends object
? T extends infer O
? { [K in keyof O]: DebugExpandRecursive<O[K]> }
: never
: T;
2 changes: 2 additions & 0 deletions frontend/apps/mobile/src/@types/modules.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
declare module "react-native-vector-icons/MaterialCommunityIcons";
declare module "react-native-vector-icons/MaterialIcons";
11 changes: 8 additions & 3 deletions frontend/apps/mobile/src/components/GroupListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ import React from "react";
import { List } from "react-native-paper";
import { useApi } from "../core/ApiProvider";
import { changeActiveGroup, selectGroupSlice, useAppDispatch, useAppSelector } from "../store";
import { RootDrawerParamList } from "../navigation/types";
import { DrawerNavigationProp } from "@react-navigation/drawer";

interface Props {
groupId: number;
}

export const GroupListItem: React.FC<Props> = ({ groupId }) => {
const navigation = useNavigation();
const navigation = useNavigation<DrawerNavigationProp<RootDrawerParamList>>();
const dispatch = useAppDispatch();
const { api } = useApi();
const group = useAppSelector((state) => selectGroupById({ state: selectGroupSlice(state), groupId }));
Expand All @@ -29,8 +31,11 @@ export const GroupListItem: React.FC<Props> = ({ groupId }) => {
.unwrap()
.then(() => {
navigation.navigate("GroupStackNavigator", {
screen: "TransactionList",
params: { groupId },
screen: "BottomTabNavigator",
params: {
screen: "TransactionList",
params: { groupId },
},
});
});
}}
Expand Down
40 changes: 21 additions & 19 deletions frontend/apps/mobile/src/components/PositionDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { selectSortedAccounts, wipPositionUpdated } from "@abrechnung/redux";
import { PositionValidationErrors, TransactionPosition, ValidationError, validatePosition } from "@abrechnung/types";
import { TransactionPosition, PositionValidator, PositionValidationErrors } from "@abrechnung/types";
import memoize from "proxy-memoize";
import React, { useCallback, useEffect, useState } from "react";
import { ScrollView, StyleSheet } from "react-native";
Expand All @@ -15,13 +15,13 @@ import {
TextInput,
useTheme,
} from "react-native-paper";
import { notify } from "../notifications";
import { RootState, selectAccountSlice, useAppDispatch, useAppSelector } from "../store";
import { NumericInput } from "./NumericInput";
import { KeyboardAvoidingDialog } from "./style/KeyboardAvoidingDialog";

interface Props {
groupId: number;
transactionId: number;
position: TransactionPosition;
editing: boolean;
showDialog: boolean;
Expand All @@ -43,8 +43,11 @@ const initialEditingState: localEditingState = {
communist_shares: 0,
};

const emptyFormErrors: PositionValidationErrors = { fieldErrors: {}, formErrors: [] };

export const PositionDialog: React.FC<Props> = ({
groupId,
transactionId,
position,
editing,
showDialog,
Expand Down Expand Up @@ -72,7 +75,7 @@ export const PositionDialog: React.FC<Props> = ({
[groupId, searchTerm, editing, localEditingState]
);
const accounts = useAppSelector(selector);
const [errors, setErrors] = useState<PositionValidationErrors>({});
const [errors, setErrors] = useState<PositionValidationErrors>(emptyFormErrors);

const toggleShare = (accountID: number) => {
const currVal = localEditingState.usages[accountID] !== undefined ? localEditingState.usages[accountID] : 0;
Expand All @@ -99,7 +102,7 @@ export const PositionDialog: React.FC<Props> = ({
usages: position.usages,
});
}
setErrors({});
setErrors(emptyFormErrors);
}, [position, setLocalEditingState, setErrors]);

useEffect(() => {
Expand All @@ -118,18 +121,13 @@ export const PositionDialog: React.FC<Props> = ({
communist_shares: localEditingState.communist_shares,
price: localEditingState.price,
};
// TODO: perform input validation
try {
validatePosition(newPosition);
dispatch(wipPositionUpdated({ groupId, transactionId: position.transactionID, position: newPosition }));
setErrors({});
const validateRet = PositionValidator.safeParse(newPosition);
if (validateRet.success) {
dispatch(wipPositionUpdated({ groupId, transactionId, position: newPosition }));
setErrors(emptyFormErrors);
onHideDialog();
} catch (err) {
if (err instanceof ValidationError) {
setErrors(err.data);
} else {
notify({ text: `Error while saving position: ${(err as Error).toString()}` });
}
} else {
setErrors(validateRet.error.flatten());
}
};

Expand Down Expand Up @@ -193,9 +191,11 @@ export const PositionDialog: React.FC<Props> = ({
editable={editing}
onChangeText={onChangeName}
style={inputStyles}
error={errors.name !== undefined}
error={errors.fieldErrors.name !== undefined}
/>
{errors.name !== undefined && <HelperText type="error">{errors.name}</HelperText>}
{errors.fieldErrors.name !== undefined && (
<HelperText type="error">{errors.fieldErrors.name}</HelperText>
)}
<NumericInput
label="Price" // TODO: proper float input
value={localEditingState.price}
Expand All @@ -204,9 +204,11 @@ export const PositionDialog: React.FC<Props> = ({
onChange={onChangePrice}
style={inputStyles}
right={<TextInput.Affix text={currency_symbol} />}
error={errors.price !== undefined}
error={errors.fieldErrors.price !== undefined}
/>
{errors.price !== undefined && <HelperText type="error">{errors.price}</HelperText>}
{errors.fieldErrors.price !== undefined && (
<HelperText type="error">{errors.fieldErrors.price}</HelperText>
)}

<List.Item
title="Communist Shares"
Expand Down
10 changes: 5 additions & 5 deletions frontend/apps/mobile/src/components/PositionListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@ import PositionDialog from "./PositionDialog";

interface Props {
groupId: number;
transactionId: number;
currency_symbol: string;
position: TransactionPosition;
editing: boolean;
}

export const PositionListItem: React.FC<Props> = ({ groupId, currency_symbol, position, editing }) => {
export const PositionListItem: React.FC<Props> = ({ groupId, transactionId, currency_symbol, position, editing }) => {
const theme = useTheme();
const dispatch = useAppDispatch();
const [showPositionDialog, setShowPositionDialog] = useState(false);
Expand All @@ -26,11 +27,11 @@ export const PositionListItem: React.FC<Props> = ({ groupId, currency_symbol, po
const closeMenu = () => setShowMenu(false);

const onDeletePosition = () => {
dispatch(positionDeleted({ groupId, transactionId: position.transactionID, positionId: position.id }));
dispatch(positionDeleted({ groupId, transactionId, positionId: position.id }));
};

const onCopyPosition = () => {
dispatch(wipPositionAdded({ groupId, transactionId: position.transactionID, position }));
dispatch(wipPositionAdded({ groupId, transactionId, position }));
};

return (
Expand Down Expand Up @@ -64,6 +65,7 @@ export const PositionListItem: React.FC<Props> = ({ groupId, currency_symbol, po
<Portal>
<PositionDialog
groupId={groupId}
transactionId={transactionId}
position={position}
editing={editing}
showDialog={showPositionDialog}
Expand All @@ -74,5 +76,3 @@ export const PositionListItem: React.FC<Props> = ({ groupId, currency_symbol, po
</>
);
};

export default PositionListItem;
5 changes: 5 additions & 0 deletions frontend/apps/mobile/src/components/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export * from "./LoadingIndicator";
export * from "./NumericInput";
export * from "./DateTimeInput";
export * from "./CurrencySelect";
export * from "./tag-select";
Loading

0 comments on commit aa0af67

Please sign in to comment.