Skip to content

Commit

Permalink
Merge pull request #779 from tonkeeper/release/4.1.0
Browse files Browse the repository at this point in the history
Release (4.1.0)
  • Loading branch information
sorokin0andrey authored Apr 8, 2024
2 parents f18d810 + 0bbfc9c commit ee2d466
Show file tree
Hide file tree
Showing 43 changed files with 647 additions and 43 deletions.
2 changes: 1 addition & 1 deletion packages/mobile/android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ android {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 433
versionName "4.0.1"
versionName "4.1.0"
missingDimensionStrategy 'react-native-camera', 'general'
missingDimensionStrategy 'store', 'play'
}
Expand Down
16 changes: 15 additions & 1 deletion packages/mobile/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import crashlytics from '@react-native-firebase/crashlytics';
import messaging from '@react-native-firebase/messaging';
import { withIAPContext } from 'react-native-iap';
import { startApp } from './src/index';
import { tk } from './src/wallet';

LogBox.ignoreLogs([
'Non-serializable values were found in the navigation state',
Expand All @@ -32,15 +33,28 @@ if (__DEV__) {
getAttachScreenFromStorage();
}

// TODO: should be address-specified store
async function handleDappMessage(remoteMessage) {
// handle data-only messages
if (remoteMessage.notification?.body) {
return null;
}
if (!['console_dapp_notification'].includes(remoteMessage.data?.type)) {

if (
!['console_dapp_notification', 'better_stake_option_found', 'collect_stake'].includes(
remoteMessage.data?.type,
)
) {
return null;
}

if (remoteMessage.data?.type === 'better_stake_option_found') {
tk.wallet.staking.toggleRestakeBanner(
true,
remoteMessage.data.stakingAddressToMigrateFrom,
);
}

await useNotificationsStore.persist.rehydrate();
useNotificationsStore.getState().actions.addNotification(
{
Expand Down
6 changes: 3 additions & 3 deletions packages/mobile/ios/ton_keeper.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -1052,7 +1052,7 @@
outputFileListPaths = (
);
outputPaths = (
"$SRCROOT/$PROJECT_NAME/Resources/Generated/R.generated.swift",
$SRCROOT/$PROJECT_NAME/Resources/Generated/R.generated.swift,
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
Expand Down Expand Up @@ -1294,7 +1294,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 4.0.1;
MARKETING_VERSION = 4.1.0;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
Expand Down Expand Up @@ -1328,7 +1328,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 4.0.1;
MARKETING_VERSION = 4.1.0;
OTHER_LDFLAGS = (
"$(inherited)",
"-ObjC",
Expand Down
2 changes: 1 addition & 1 deletion packages/mobile/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
"@craftzdog/react-native-buffer": "^6.0.5",
"@expo/react-native-action-sheet": "^4.0.1",
"@gorhom/bottom-sheet": "^4.6.0",
"@rainbow-me/animated-charts": "https://github.com/tonkeeper/react-native-animated-charts#65f723604f3abc8a05ecfa2918fe9b0b42fd8363",
"@rainbow-me/animated-charts": "https://github.com/tonkeeper/react-native-animated-charts#737b1633c41e13da437c8e111c4aedd15bd10558",
"@react-native-async-storage/async-storage": "^1.15.5",
"@react-native-community/clipboard": "^1.5.1",
"@react-native-community/netinfo": "^9.3.2",
Expand Down
36 changes: 36 additions & 0 deletions packages/mobile/src/components/RestakeBanner/IconsComposition.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { Steezy, View } from '@tonkeeper/uikit';
import { Image } from 'react-native';
import { liquidTfIconSource, tonIconSource } from '@tonkeeper/uikit/assets/staking';

export const IconsComposition = () => {
return (
<View style={styles.container}>
<Image style={styles.image.static} source={tonIconSource} />
<View>
<View style={styles.border} />
<Image style={[styles.image.static]} source={liquidTfIconSource} />
</View>
</View>
);
};

export const styles = Steezy.create(({ colors }) => ({
container: {
flexDirection: 'row',
gap: -12,
},
image: {
width: 64,
height: 64,
borderRadius: 32,
},
border: {
position: 'absolute',
top: -3,
bottom: -3,
left: -3,
right: -3,
backgroundColor: colors.backgroundContent,
borderRadius: 35,
},
}));
266 changes: 266 additions & 0 deletions packages/mobile/src/components/RestakeBanner/RestakeBanner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,266 @@
import React, { memo, useCallback, useEffect, useMemo } from 'react';
import {
Button,
Icon,
Spacer,
Steezy,
Text,
TouchableOpacity,
View,
} from '@tonkeeper/uikit';
import { t } from '@tonkeeper/shared/i18n';
import { replaceString } from '@tonkeeper/shared/utils/replaceString';
import { PoolImplementationType, PoolInfo } from '@tonkeeper/core/src/TonAPI';
import { RestakeStep } from './RestakeStep';
import { usePoolInfo } from '$hooks/usePoolInfo';
import { AppStackRouteNames } from '$navigation';
import { StakingTransactionType } from '$core/StakingSend/types';
import { useNavigation } from '@tonkeeper/router';
import { formatter } from '@tonkeeper/shared/formatter';
import { useStakingState } from '@tonkeeper/shared/hooks';
import { useStakingCycle } from '$hooks/useStakingCycle';
import { LayoutAnimation } from 'react-native';
import { tk } from '$wallet';
import { Address } from '@tonkeeper/core';
import { IconsComposition } from './IconsComposition';
import { useFiatValue } from '$hooks/useFiatValue';
import { CryptoCurrencies } from '$shared/constants';
import { stakingFormatter } from '$utils/formatter';

export interface ExtendedPoolInfo extends PoolInfo {
isWithdrawal: boolean;
balance: string | undefined;
}

export interface RestakeBannerProps {
poolsList: ExtendedPoolInfo[];
migrateFrom: string;
}

export enum RestakeSteps {
UNSTAKE = 1,
WAIT_FOR_WITHDRAWAL = 2,
STAKE_INTO_TONSTAKERS = 3,
DONE = 4,
}

export const RestakeBanner = memo<RestakeBannerProps>((props) => {
const nav = useNavigation();
const tonstakersPool = useMemo(() => {
return props.poolsList.find(
(pool) => pool.implementation === PoolImplementationType.LiquidTF,
) as ExtendedPoolInfo;
}, [props.poolsList]);
const { handleTopUpPress } = usePoolInfo(tonstakersPool);

const handleCloseRestakeBanner = useCallback(() => {
LayoutAnimation.easeInEaseOut();
tk.wallet.staking.toggleRestakeBanner(false);
}, []);

const poolToWithdrawal = useMemo(
() =>
props.poolsList.find((pool) => Address.compare(pool.address, props.migrateFrom)),
[props.migrateFrom, props.poolsList],
);
const toWithdrawalStakingInfo = useStakingState(
(s) => poolToWithdrawal && s.stakingInfo[poolToWithdrawal.address],
[poolToWithdrawal],
);

const bypassStakeStep = useStakingState((s) => s.bypassStakeStep, []);

const readyWithdraw = useFiatValue(
CryptoCurrencies.Ton,
stakingFormatter.fromNano(toWithdrawalStakingInfo?.ready_withdraw ?? '0'),
);

const isReadyWithdraw = readyWithdraw.amount !== '0';

const handleWithdrawal = useCallback(
(pool: ExtendedPoolInfo, withdrawAll?: boolean) => () => {
nav.push(AppStackRouteNames.StakingSend, {
amount: withdrawAll && pool.balance,
poolAddress: pool.address,
transactionType:
pool.implementation === PoolImplementationType.Tf
? StakingTransactionType.WITHDRAWAL_CONFIRM
: StakingTransactionType.WITHDRAWAL,
});
},
[nav],
);

const handleCollectWithdrawal = useCallback(
(pool: ExtendedPoolInfo) => () => {
nav.push(AppStackRouteNames.StakingSend, {
poolAddress: pool.address,
transactionType: StakingTransactionType.WITHDRAWAL_CONFIRM,
});
},
[nav],
);

const isWaitingForWithdrawal = toWithdrawalStakingInfo?.pending_withdraw;
const currentStepId = useMemo(() => {
if (tonstakersPool?.balance) {
return RestakeSteps.DONE;
}
// Go to last step if pool to withdrawal is empty now (or if balance so small, or step is bypassed)
if (bypassStakeStep || Number(poolToWithdrawal?.balance) < 0.1) {
return RestakeSteps.STAKE_INTO_TONSTAKERS;
}
// If user has pending withdrawal, render step with waiting
if (isWaitingForWithdrawal || readyWithdraw.amount !== '0') {
return RestakeSteps.WAIT_FOR_WITHDRAWAL;
}
return RestakeSteps.UNSTAKE;
}, [
bypassStakeStep,
isWaitingForWithdrawal,
poolToWithdrawal?.balance,
readyWithdraw.amount,
tonstakersPool?.balance,
]);

useEffect(() => {
if (currentStepId === RestakeSteps.DONE) {
handleCloseRestakeBanner();
}
}, [currentStepId, handleCloseRestakeBanner]);

const { formattedDuration, isCooldown } = useStakingCycle(
poolToWithdrawal?.cycle_start,
poolToWithdrawal?.cycle_end,
isWaitingForWithdrawal,
);

const renderFormattedDuration = useCallback(() => {
return (
<Text type="body1" color="textPrimary" style={{ fontVariant: ['tabular-nums'] }}>
{formattedDuration}
</Text>
);
}, [formattedDuration]);

const waitForWithdrawalText = useMemo(() => {
if (currentStepId !== RestakeSteps.WAIT_FOR_WITHDRAWAL) {
return t('restake_banner.wait_step');
}
if (isReadyWithdraw) {
return t('restake_banner.wait_step_withdraw');
}
if (isCooldown) {
return t('restake_banner.wait_step_cooldown');
}
return replaceString(
t('restake_banner.wait_step_pending'),
'%duration',
renderFormattedDuration,
);
}, [currentStepId, isCooldown, isReadyWithdraw, renderFormattedDuration]);

return (
<View style={styles.container}>
<IconsComposition />
<TouchableOpacity
onPress={handleCloseRestakeBanner}
activeOpacity={0.95}
style={styles.absoluteCloseButton}
>
<Icon name={'ic-xmark-outline-28'} color="iconTertiary" />
</TouchableOpacity>
<Spacer y={20} />
<Text type="h3">
{replaceString(t('restake_banner.title'), '%apy', () => (
<Text type="h3" color="accentBlue">
APY {tonstakersPool.apy.toFixed(2)}%
</Text>
))}
</Text>
<Spacer y={12} />
<View>
<RestakeStep
completed={currentStepId > RestakeSteps.UNSTAKE}
actions={
poolToWithdrawal &&
currentStepId === RestakeSteps.UNSTAKE && [
<Button
color="primary"
size="small"
onPress={handleWithdrawal(poolToWithdrawal, true)}
title={t('restake_banner.unstake_action_all', {
amount: formatter.format(poolToWithdrawal.balance, {
currency: 'TON',
}),
})}
/>,
<Button
color="tertiary"
size="small"
onPress={handleWithdrawal(poolToWithdrawal)}
title={t('restake_banner.unstake_action_manual')}
/>,
]
}
stepId={RestakeSteps.UNSTAKE}
text={t('restake_banner.unstake_step')}
/>
<RestakeStep
completed={currentStepId > RestakeSteps.WAIT_FOR_WITHDRAWAL}
stepId={RestakeSteps.WAIT_FOR_WITHDRAWAL}
text={waitForWithdrawalText}
actions={
currentStepId <= RestakeSteps.WAIT_FOR_WITHDRAWAL &&
isReadyWithdraw && [
<Button
size="small"
color={'primary'}
onPress={handleCollectWithdrawal(poolToWithdrawal!)}
title={t('restake_banner.wait_step_collect')}
/>,
]
}
/>
<RestakeStep
completed={currentStepId > RestakeSteps.STAKE_INTO_TONSTAKERS}
actionsContainerStyle={styles.lastActionsContainer}
stepId={RestakeSteps.STAKE_INTO_TONSTAKERS}
actions={[
<Button
size="small"
color={
currentStepId === RestakeSteps.STAKE_INTO_TONSTAKERS
? 'primary'
: 'tertiary'
}
onPress={handleTopUpPress}
title={t('restake_banner.stake_into_tonstakers_action')}
/>,
]}
text={t('restake_banner.stake_into_step')}
/>
</View>
</View>
);
});

const styles = Steezy.create(({ colors }) => ({
container: {
backgroundColor: colors.backgroundContent,
borderRadius: 16,
padding: 24,
},
lastActionsContainer: {
paddingBottom: 0,
},
absoluteCloseButton: {
position: 'absolute',
right: 0,
top: 0,
width: 76,
height: 76,
alignItems: 'center',
justifyContent: 'center',
},
}));
Loading

0 comments on commit ee2d466

Please sign in to comment.