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

feat: reactive price screen #3271

Merged
merged 1 commit into from
May 16, 2024
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
223 changes: 139 additions & 84 deletions app/components/price-history/price-history.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,28 @@
import { TextStyle, ViewStyle } from "node_modules/@types/react-native/index"
import * as React from "react"
import { ActivityIndicator, StyleProp, View } from "react-native"
import {
ActivityIndicator,
StyleProp,
View,
TextInput,
type TextInputProps,
} from "react-native"
import { CartesianChart, Line, useChartPressState } from "victory-native"
import type { SharedValue } from "react-native-reanimated"
import Reanimated, {
useAnimatedProps,
useDerivedValue,
type SharedValue,
} from "react-native-reanimated"

import { gql } from "@apollo/client"
import { PricePoint, useBtcPriceListQuery } from "@app/graphql/generated"
import { PricePoint, WalletCurrency, useBtcPriceListQuery } from "@app/graphql/generated"
import { useI18nContext } from "@app/i18n/i18n-react"
import { testProps } from "@app/utils/testProps"
import { Button } from "@rneui/base"
import { Text, makeStyles, useTheme } from "@rneui/themed"
import { Circle } from "@shopify/react-native-skia"

const multiple = (currentUnit: string) => {
switch (currentUnit) {
case "USDCENT":
return 10 ** -5
default:
return 1
}
}
import { GaloyErrorBox } from "../atomic/galoy-error-box"
import { useDisplayCurrency } from "@app/hooks/use-display-currency"

const GraphRange = {
ONE_DAY: "ONE_DAY",
Expand Down Expand Up @@ -49,7 +52,6 @@ export const PriceHistory = () => {
const {
theme: { colors },
} = useTheme()

const { LL } = useI18nContext()
const [graphRange, setGraphRange] = React.useState<GraphRangeType>(GraphRange.ONE_DAY)

Expand All @@ -61,55 +63,59 @@ export const PriceHistory = () => {

const { state, isActive } = useChartPressState({ x: 0, y: { y: 0 } })

function ToolTip({ x, y }: { x: SharedValue<number>; y: SharedValue<number> }) {
return <Circle cx={x} cy={y} r={8} color={colors.secondary} />
}

if (error) {
return <Text>{`${error}`}</Text>
}
const { formatMoneyAmount } = useDisplayCurrency()

if (loading || data === null || data?.btcPriceList === null) {
function ToolTip({ x, y }: { x: SharedValue<number>; y: SharedValue<number> }) {
return (
<View style={styles.verticalAlignment}>
<ActivityIndicator animating size="large" color={colors.primary} />
</View>
<>
<Circle cx={x} cy={y} r={8} color={colors.primary} />
</>
)
}

const ranges = GraphRange[graphRange]
const rangeTimestamps = {
ONE_DAY: 300,
ONE_WEEK: 1800,
ONE_MONTH: 86400,
ONE_YEAR: 86400,
FIVE_YEARS: 86400,
}

const lastPrice = priceList && priceList[priceList.length - 1]
if (!loading && lastPrice) {
const timeDiff = Date.now() / 1000 - lastPrice.timestamp
if (timeDiff > rangeTimestamps[ranges]) {
setGraphRange(ranges)
}
}

const prices = priceList
.filter((price) => price !== null)
.map((price) => price as PricePoint)
// FIXME: backend should be updated so that PricePoint is non-nullable
.map((index) => {
const amount = Math.floor(index.price.base / 10 ** index.price.offset)

const currentPriceData = prices[prices.length - 1].price
const startPriceData = prices[0].price
const price =
(currentPriceData.base / 10 ** currentPriceData.offset) *
multiple(currentPriceData.currencyUnit)
const delta = currentPriceData.base / startPriceData.base - 1
return {
y: amount,
formattedAmount: formatMoneyAmount({
moneyAmount: {
amount,
currency: WalletCurrency.Usd,
currencyCode: "USDCENT",
},
}),
timestamp: index.timestamp,
currencyUnit: index.price.currencyUnit,
}
})

const currentPriceData = prices[prices.length - 1]?.y
const startPriceData = prices[0]?.y
const delta =
currentPriceData && startPriceData
? (currentPriceData - startPriceData) / startPriceData
: 0
const color = delta > 0 ? { color: colors._green } : { color: colors.red }

const prices2 = prices.map((index) => ({
y: (index.price.base / 10 ** index.price.offset) * multiple(index.price.currencyUnit),
}))
const activePrice = useDerivedValue(() => {
const price = isActive
? prices.find((price) => price.y === state.y.y.value.value)
: prices[prices.length - 1]

return price?.formattedAmount ?? ""
})

const activeTimestamp = useDerivedValue(() => {
const timestamp = isActive
? prices.find((price) => price.y === state.y.y.value.value)?.timestamp
: undefined

return `${timestamp ? new Date(timestamp * 1000).toLocaleString(undefined, { year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit" }) : ""}`
})

const label = () => {
switch (graphRange) {
Expand All @@ -136,36 +142,38 @@ export const PriceHistory = () => {
const titleStyleForRange = (titleGraphRange: GraphRangeType): StyleProp<TextStyle> => {
return graphRange === titleGraphRange ? null : styles.titleStyleTime
}

return (
<View style={styles.verticalAlignment}>
<View>
<View {...testProps(LL.PriceHistoryScreen.satPrice())} style={styles.textView}>
<Text type="p1">{LL.PriceHistoryScreen.satPrice()}</Text>
<Text type="p1" bold>
${price.toFixed(2)}
</Text>
</View>
<View style={styles.textView}>
<Text type="p1" style={[styles.delta, color]}>
{(delta * 100).toFixed(2)}%{" "}
</Text>
<Text type="p1" {...testProps("range")}>
{label()}
</Text>
<View style={styles.screen}>
<View {...testProps(LL.PriceHistoryScreen.satPrice())} style={styles.textView}>
<AnimText
// @ts-ignore-next-line
text={activePrice}
style={styles.priceText}
color={colors.black}
/>
<View style={styles.subtextContainer}>
{!isActive && !loading ? (
<Text type="p1" {...testProps("range")}>
<Text type="p1" style={[styles.delta, color]}>
{(delta * 100).toFixed(2)}%{" "}
</Text>
{label()}
</Text>
) : (
<AnimText
// @ts-ignore-next-line
text={activeTimestamp}
style={styles.subtext}
color={colors.black}
/>
)}
</View>
</View>
<View style={styles.chart}>
{
{!loading && data ? (
/* eslint @typescript-eslint/ban-ts-comment: "off" */
// @ts-ignore-next-line no-implicit-any error
<CartesianChart
data={prices2}
yKeys={["y"]}
chartPressState={state}
padding={{ left: 20, right: 20 }}
domainPadding={{ bottom: 10, top: 10 }}
>
<CartesianChart data={prices} yKeys={["y"]} chartPressState={state}>
{({ points }) => (
<>
<Line
Expand All @@ -174,11 +182,20 @@ export const PriceHistory = () => {
strokeWidth={2}
curveType="natural"
/>
{isActive && <ToolTip x={state.x.position} y={state.y.y.position} />}

{isActive && (
<>
<ToolTip x={state.x.position} y={state.y.y.position} />
</>
)}
</>
)}
</CartesianChart>
}
) : (
<View style={styles.verticalAlignment}>
<ActivityIndicator animating size="large" color={colors.primary} />
</View>
)}
</View>
<View style={styles.pricesContainer}>
<Button
Expand Down Expand Up @@ -228,18 +245,54 @@ export const PriceHistory = () => {
onPress={() => setGraphRange(GraphRange.FIVE_YEARS)}
/>
</View>
{error && <GaloyErrorBox errorMessage={error.message} />}
</View>
)
}

const AnimText = Reanimated.createAnimatedComponent(TextInput)
Reanimated.addWhitelistedNativeProps({ text: true })

type AnimatedTextProps = Omit<TextInputProps, "editable" | "value"> & {
text: SharedValue<string>
style?: React.ComponentProps<typeof AnimText>["style"]
}

export function AnimatedText({ text, ...rest }: AnimatedTextProps) {
const animProps = useAnimatedProps(() => {
return {
text: text.value,
}
})

return (
<AnimText
{...rest}
value={text.value}
// @ts-ignore
animatedProps={animProps}
editable={false}
/>
)
}

const useStyles = makeStyles(({ colors }) => ({
buttonStyleTime: {
backgroundColor: colors.transparent,
borderRadius: 40,
width: 48,
height: 48,
},

subtextContainer: {
height: 24,
},
priceText: {
fontSize: 32,
},
subtext: {
fontSize: 18,
lineHeight: 24,
},
buttonStyleTimeActive: {
backgroundColor: colors.primary,
borderRadius: 40,
Expand All @@ -248,7 +301,7 @@ const useStyles = makeStyles(({ colors }) => ({
},

chart: {
height: "70%",
height: "60%",
},

delta: {
Expand All @@ -262,16 +315,18 @@ const useStyles = makeStyles(({ colors }) => ({
},

textView: {
alignSelf: "center",
flexDirection: "row",
paddingHorizontal: 16,
marginBottom: 16,
},

titleStyleTime: {
color: colors.grey3,
},

screen: {
paddingVertical: 16,
},
verticalAlignment: {
flex: 1,
justifyContent: "space-between",
alignItems: "center",
justifyContent: "center",
},
}))
1 change: 1 addition & 0 deletions app/graphql/generated.gql
Original file line number Diff line number Diff line change
Expand Up @@ -780,6 +780,7 @@ mutation userUpdateUsername($input: UserUpdateUsernameInput!) {

query Bulletins($first: Int!, $after: String) {
me {
id
unacknowledgedStatefulNotificationsWithBulletinEnabled(
first: $first
after: $after
Expand Down
3 changes: 2 additions & 1 deletion app/graphql/generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2690,7 +2690,7 @@ export type BulletinsQueryVariables = Exact<{
}>;


export type BulletinsQuery = { readonly __typename: 'Query', readonly me?: { readonly __typename: 'User', readonly unacknowledgedStatefulNotificationsWithBulletinEnabled: { readonly __typename: 'StatefulNotificationConnection', readonly pageInfo: { readonly __typename: 'PageInfo', readonly endCursor?: string | null, readonly hasNextPage: boolean, readonly hasPreviousPage: boolean, readonly startCursor?: string | null }, readonly edges: ReadonlyArray<{ readonly __typename: 'StatefulNotificationEdge', readonly cursor: string, readonly node: { readonly __typename: 'StatefulNotification', readonly id: string, readonly title: string, readonly body: string, readonly createdAt: number, readonly acknowledgedAt?: number | null, readonly bulletinEnabled: boolean, readonly icon?: Icon | null, readonly action?: { readonly __typename: 'OpenDeepLinkAction', readonly deepLink: string } | { readonly __typename: 'OpenExternalLinkAction', readonly url: string } | null } }> } } | null };
export type BulletinsQuery = { readonly __typename: 'Query', readonly me?: { readonly __typename: 'User', readonly id: string, readonly unacknowledgedStatefulNotificationsWithBulletinEnabled: { readonly __typename: 'StatefulNotificationConnection', readonly pageInfo: { readonly __typename: 'PageInfo', readonly endCursor?: string | null, readonly hasNextPage: boolean, readonly hasPreviousPage: boolean, readonly startCursor?: string | null }, readonly edges: ReadonlyArray<{ readonly __typename: 'StatefulNotificationEdge', readonly cursor: string, readonly node: { readonly __typename: 'StatefulNotification', readonly id: string, readonly title: string, readonly body: string, readonly createdAt: number, readonly acknowledgedAt?: number | null, readonly bulletinEnabled: boolean, readonly icon?: Icon | null, readonly action?: { readonly __typename: 'OpenDeepLinkAction', readonly deepLink: string } | { readonly __typename: 'OpenExternalLinkAction', readonly url: string } | null } }> } } | null };

export type BusinessMapMarkersQueryVariables = Exact<{ [key: string]: never; }>;

Expand Down Expand Up @@ -4779,6 +4779,7 @@ export type HomeUnauthedQueryResult = Apollo.QueryResult<HomeUnauthedQuery, Home
export const BulletinsDocument = gql`
query Bulletins($first: Int!, $after: String) {
me {
id
unacknowledgedStatefulNotificationsWithBulletinEnabled(
first: $first
after: $after
Expand Down
1 change: 1 addition & 0 deletions app/screens/home-screen/home-screen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ gql`
query Bulletins($first: Int!, $after: String) {
me {
id
unacknowledgedStatefulNotificationsWithBulletinEnabled(
first: $first
after: $after
Expand Down
1 change: 1 addition & 0 deletions app/screens/price/price-history-screen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export const PriceHistoryScreen: React.FC = () => {
}, [accountId])

const styles = useStyles()

return (
<Screen preset="scroll" style={styles.screen}>
<PriceHistory />
Expand Down
7 changes: 0 additions & 7 deletions e2e/06-other-tests.e2e.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,13 +154,6 @@ describe("Price graph flow", () => {
await clickIcon("graph")
})

it("check if price graph header is shown", async () => {
const priceGraphHeader = await $(selector(LL.PriceHistoryScreen.satPrice(), "Other"))
const rangeText = await $(selector("range", "StaticText"))
await priceGraphHeader.waitForDisplayed({ timeout })
expect(rangeText).toBeDisplayed()
})

it("click on one week button", async () => {
const oneWeekButton = await $(selector(LL.PriceHistoryScreen.oneWeek(), "Button"))
const rangeText = await $(selector("range", "StaticText"))
Expand Down
Loading