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

Add Custom Collection #522

Merged
merged 11 commits into from
Nov 2, 2022
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
1 change: 0 additions & 1 deletion android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,5 @@ allprojects {

google()
maven { url 'https://www.jitpack.io' }
jcenter()
}
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
"@dfinity/principal": "^0.9.3",
"@hookform/error-message": "^2.0.0",
"@psychedelic/dab-js": "1.4.12",
"@psychedelic/plug-controller": "0.24.5",
"@psychedelic/plug-controller": "0.24.9",
"@react-native-async-storage/async-storage": "^1.17.10",
"@react-native-community/blur": "^4.2.0",
"@react-native-community/clipboard": "^1.5.1",
Expand Down
19 changes: 19 additions & 0 deletions src/components/buttons/ScrollableButton/hooks/useScrollHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { useState } from 'react';
import { NativeScrollEvent, NativeSyntheticEvent } from 'react-native';

function useScrollHanlder() {
const [scrollPosition, setScrollPosition] = useState(0);

const handleOnScroll = (
scrollEvent: NativeSyntheticEvent<NativeScrollEvent>
) => {
setScrollPosition(scrollEvent?.nativeEvent?.contentOffset.y);
};

return {
scrollPosition,
handleOnScroll,
};
}

export default useScrollHanlder;
mattgle marked this conversation as resolved.
Show resolved Hide resolved
84 changes: 84 additions & 0 deletions src/components/buttons/ScrollableButton/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import React, { useEffect, useState } from 'react';
import { StyleProp, TextStyle, View, ViewStyle } from 'react-native';
import Animated, {
Easing,
useAnimatedStyle,
useSharedValue,
withTiming,
} from 'react-native-reanimated';

import { Touchable } from '@/components/common';
import AddGradient from '@/icons/svg/AddGradient.svg';

import styles from './styles';

interface Props {
onPress: () => void;
text: string;
textWidth: number;
buttonStyle?: StyleProp<ViewStyle>;
imageStyle?: StyleProp<ViewStyle>;
textStyle?: StyleProp<TextStyle>;
scrollPosition: number;
}

function ScrollableButton({
onPress,
text,
textWidth,
textStyle,
buttonStyle,
imageStyle,
scrollPosition,
}: Props) {
const [showFullButton, setShowFullButton] = useState(true);
const [currentScrollPosition, setCurrentScrollPosition] = useState(0);

const animatedWidth = useSharedValue(textWidth);

const animatedStyle = useAnimatedStyle(() => {
return {
width: withTiming(animatedWidth.value, {
duration: 200,
easing: Easing.bezier(0.25, 0.1, 0.25, 1),
}),
opacity: withTiming(animatedWidth.value > 0 ? 1 : 0, {
duration: 450,
}),
};
});

useEffect(() => {
if (currentScrollPosition < scrollPosition) {
// Scroll down.
setShowFullButton(false);
animatedWidth.value = 0;
} else if (scrollPosition < currentScrollPosition || scrollPosition === 0) {
// Scroll up or start position.
setShowFullButton(true);
animatedWidth.value = textWidth;
}
setCurrentScrollPosition(scrollPosition);
}, [scrollPosition]);

return (
<Touchable onPress={onPress}>
<View style={[styles.button, buttonStyle]}>
<AddGradient width={18} height={18} style={imageStyle} />
<Animated.Text
ellipsizeMode="clip"
numberOfLines={1}
style={[
styles.text,
animatedStyle,
showFullButton && styles.marginText,
textStyle,
]}>
{text}
</Animated.Text>
</View>
</Touchable>
);
}

export default ScrollableButton;
30 changes: 30 additions & 0 deletions src/components/buttons/ScrollableButton/styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { StyleSheet } from 'react-native';

import { Colors, FontStyles } from '@/constants/theme';
import { fontMaker } from '@/utils/fonts';

export default StyleSheet.create({
button: {
minWidth: 48,
height: 48,
flexDirection: 'row',
alignItems: 'center',
backgroundColor: Colors.ActionBlue,
borderRadius: 100,
paddingHorizontal: 16,
shadowColor: Colors.White.Pure,
shadowOffset: { width: 2, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 12,
elevation: 4,
},
disabled: {
opacity: 0.2,
},
text: {
...fontMaker({ ...FontStyles.Body2, color: Colors.White.Pure }),
},
marginText: {
marginLeft: 8,
},
});
9 changes: 9 additions & 0 deletions src/components/icons/svg/AddGradient.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 2 additions & 5 deletions src/constants/nfts.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,2 @@
import { isAndroid } from './platform';

// Since we're having problems with Apple's approval we're disabling NFTs until we find a solution

export const ENABLE_NFTS = isAndroid;
// TODO: Delete ENABLE_NFTS everywhere.
export const ENABLE_NFTS = true;
2 changes: 2 additions & 0 deletions src/constants/urls.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
export const dabFormUrl = 'https://dab-ooo.typeform.com/token-list';
export const customTokensUrl =
'https://docs.plugwallet.ooo/getting-started/custom-tokens/';
export const custonNFTsUrl =
'https://docs.plugwallet.ooo/getting-started/nfts/';

export const docsUrl = 'https://docs.plugwallet.ooo';
export const blogUrl = 'https://medium.com/plugwallet';
Expand Down
5 changes: 5 additions & 0 deletions src/interfaces/keyring.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { WalletNFTInfo } from '@psychedelic/plug-controller/dist/interfaces/plug_wallet';

export type FungibleStandard = 'DIP20' | 'EXT';
export type NonFungibleStandard = 'DIP721';
export type Standard = 'DIP20' | 'XTC' | 'WICP' | 'EXT' | 'ICP' | string;

export interface StandardToken {
Expand Down Expand Up @@ -27,3 +30,5 @@ export interface InferredTransaction {
};
caller: string;
}

export interface CollectionInfo extends WalletNFTInfo {}
80 changes: 77 additions & 3 deletions src/redux/slices/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@ import { createAsyncThunk, createSlice, isAnyOf } from '@reduxjs/toolkit';

import { JELLY_CANISTER_ID } from '@/constants/canister';
import { ENABLE_NFTS } from '@/constants/nfts';
import { FungibleStandard, TokenBalance } from '@/interfaces/keyring';
import {
CollectionInfo,
FungibleStandard,
NonFungibleStandard,
TokenBalance,
} from '@/interfaces/keyring';
import {
Asset,
Collection,
Expand Down Expand Up @@ -301,7 +306,7 @@ export const editContact = createAsyncThunk<
'user/editContact',
async ({ contact, newContact }, { getState, rejectWithValue }) => {
try {
const state = getState();
const state = getState() as State;
const instance = KeyRing.getInstance();
const removeContactRes = await instance?.deleteContact({
addressName: contact.name,
Expand Down Expand Up @@ -351,7 +356,6 @@ export const addCustomToken = createAsyncThunk<
onSuccess?.();
return user.assets;
}

const registeredToken = await instance?.registerToken({
canisterId: canisterId.toString(),
standard,
Expand Down Expand Up @@ -421,6 +425,66 @@ export const getTokenInfo = createAsyncThunk(
}
);

export const getCollectionInfo = createAsyncThunk(
'user/getCollectionInfo',
async (
{
collection,
onSuccess,
onFailure,
}: {
collection: { canisterId: string; standard: NonFungibleStandard };
onSuccess: (collectionInfo: CollectionInfo) => void;
onFailure: () => void;
},
{ rejectWithValue }
) => {
const instance = KeyRing.getInstance();
try {
const collectionInfo = await instance?.getNFTInfo(collection);
onSuccess(collectionInfo);
} catch (e: any) {
console.log('Error while fetching Collection info', e);
onFailure?.();
return rejectWithValue(e.message);
}
}
);

// eslint-disable-next-line no-spaced-func
export const addCustomCollection = createAsyncThunk<
Collection[],
{
nft: { canisterId: string; standard: NonFungibleStandard };
onSuccess: () => void;
onFailure: (e: string) => void;
},
{ rejectValue: string; state: State }
>(
'user/addCustomCollection',
async ({ nft, onSuccess, onFailure }, { rejectWithValue, getState }) => {
const state = getState();
const instance = KeyRing.getInstance();
try {
const registeredCollection = recursiveParseBigint(
await instance?.registerNFT(nft)
);

const totalCollections = [
...state.user.collections,
registeredCollection,
] as Collection[];

onSuccess();
return totalCollections;
} catch (e: any) {
onFailure(e.message);
console.log('Error while adding custom collection:', e);
return rejectWithValue(e.message);
}
}
);

export const addConnectedApp = createAsyncThunk(
'user/addConnectedApp',
async (
Expand Down Expand Up @@ -537,6 +601,16 @@ export const userSlice = createSlice({
state.collectionsError = action.payload;
state.collectionsLoading = false;
})
.addCase(addCustomCollection.fulfilled, (state, action) => {
state.collections = action.payload ?? [];
state.collectionsLoading = false;
})
.addCase(addCustomCollection.pending, state => {
state.collectionsLoading = true;
})
.addCase(addCustomCollection.rejected, state => {
state.collectionsLoading = false;
})
.addCase(getTransactions.pending, state => {
state.transactionsError = undefined;
state.transactionsLoading = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ function RequestCall(props: Props) {
<Text
onPress={goToLearnMore}
style={[FontStyles.Normal, styles.learnMore]}>
{t('walletConnect.learnMore')}
{t('common.learnMore')}
</Text>
</View>
)}
Expand Down
76 changes: 76 additions & 0 deletions src/screens/tabs/NFTs/components/AddCollection/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { t } from 'i18next';
import React, { useRef, useState } from 'react';
import { Modalize } from 'react-native-modalize';

import ScrollableButton from '@/components/buttons/ScrollableButton';
import { ActionButton, Header, Modal, Text } from '@/components/common';
import { CollectionInfo } from '@/interfaces/keyring';

import CustomCollection from '../CustomCollection';
import ReviewCollection from '../ReviewCollection';
import styles from './styles';

interface Props {
scrollPosition: number;
}

function AddCollection({ scrollPosition }: Props) {
const modalRef = useRef<Modalize>(null);
const [selectedCollection, setSelectedCollection] =
useState<CollectionInfo>();
const showReviewCollection = !!selectedCollection;

const handleModalClose = () => {
modalRef.current?.close();
cleanState();
};

const cleanState = () => setSelectedCollection(undefined);

return (
<>
<ScrollableButton
text={t('addCollection.title')}
textWidth={112}
scrollPosition={scrollPosition}
onPress={() => modalRef?.current?.open()}
buttonStyle={styles.buttonContainer}
/>
<Modal
onClosed={cleanState}
modalRef={modalRef}
disableScrollIfPossible={false}
adjustToContentHeight
HeaderComponent={
<Header
left={
showReviewCollection && (
<ActionButton onPress={cleanState} label={t('common.back')} />
)
}
center={
<Text style={styles.title} type="subtitle3">
{t('addCollection.customCollection')}
</Text>
}
right={
<ActionButton
label={t('common.close')}
onPress={handleModalClose}
/>
}
/>
}>
{showReviewCollection ? (
<ReviewCollection
collection={selectedCollection}
handleModalClose={handleModalClose}
/>
) : (
<CustomCollection setSelectedCollection={setSelectedCollection} />
)}
</Modal>
</>
);
}
export default AddCollection;
Loading