diff --git a/mobile/README.md b/mobile/README.md
index e7fb66a7..b76118ee 100644
--- a/mobile/README.md
+++ b/mobile/README.md
@@ -5,6 +5,7 @@
The dSocial mobile app uses Expo. You can review the general expo requirements:
- Expo Requiments: https://docs.expo.dev/get-started/installation/
+- Configure buff registry: `$ npm config set @buf:registry https://buf.build/gen/npm/v1/`
Here are specific steps to install the requirements on your platform.
@@ -143,3 +144,7 @@ The manual release process uses the [`eas`](https://docs.expo.dev/build/setup/#i
3. After the build is complete, submit it to the Play Store running `eas submit --platform android`
You'll need to have a [service account json file](https://developers.google.com/android/management/service-account) to authenticate with Google Play Store.
+## Opening the App using Links
+
+You can open this app using [Linking](https://docs.expo.dev/guides/linking/).
+To understand the URL format, please refer to the 'expo-linking' usage in this project.
diff --git a/mobile/app/[account]/followers.tsx b/mobile/app/[account]/followers.tsx
index 3d2be098..40b41b95 100644
--- a/mobile/app/[account]/followers.tsx
+++ b/mobile/app/[account]/followers.tsx
@@ -1,5 +1,5 @@
-import { selectFollowers } from "redux/features/profileSlice";
-import { useAppSelector } from "@gno/redux";
+import { selectFollowers, setProfileAccountName } from "redux/features/profileSlice";
+import { useAppDispatch, useAppSelector } from "@gno/redux";
import { Following } from "@gno/types";
import FollowModalContent from "@gno/components/view/follow";
import { useRouter } from "expo-router";
@@ -7,9 +7,11 @@ import { useRouter } from "expo-router";
export default function FollowersPage() {
const data = useAppSelector(selectFollowers);
const router = useRouter();
+ const dispatch = useAppDispatch();
- const onPress = (item: Following) => {
- router.navigate({ pathname: "account", params: { accountName: item.user?.name } });
+ const onPress = async (item: Following) => {
+ await dispatch(setProfileAccountName(item.user?.name));
+ router.navigate({ pathname: "account"});
};
return ;
diff --git a/mobile/app/[account]/following.tsx b/mobile/app/[account]/following.tsx
index 44e5d268..0043a72d 100644
--- a/mobile/app/[account]/following.tsx
+++ b/mobile/app/[account]/following.tsx
@@ -1,15 +1,16 @@
-import { useAppSelector } from "@gno/redux";
+import { useAppDispatch, useAppSelector, selectFollowing, setProfileAccountName } from "@gno/redux";
import { Following } from "@gno/types";
-import { selectFollowing } from "redux/features/profileSlice";
import FollowModalContent from "@gno/components/view/follow";
import { useRouter } from "expo-router";
export default function FollowingPage() {
const data = useAppSelector(selectFollowing);
const router = useRouter();
+ const dispatch = useAppDispatch();
- const onPress = (item: Following) => {
- router.navigate({ pathname: "account", params: { accountName: item.user?.name } });
+ const onPress = async (item: Following) => {
+ await dispatch(setProfileAccountName(item.user?.name));
+ router.navigate({ pathname: "account" });
};
return ;
diff --git a/mobile/app/[account]/index.tsx b/mobile/app/[account]/index.tsx
index 88af8762..b1b5a164 100644
--- a/mobile/app/[account]/index.tsx
+++ b/mobile/app/[account]/index.tsx
@@ -1,12 +1,11 @@
-import { useEffect, useState } from "react";
+import { useCallback, useEffect, useState } from "react";
import { useDispatch } from "react-redux";
-import { router, useLocalSearchParams, useNavigation } from "expo-router";
+import { router, useNavigation, usePathname } from "expo-router";
import { AccountView } from "@gno/components/view";
import { useSearch } from "@gno/hooks/use-search";
import { Following, Post, User } from "@gno/types";
-import { setPostToReply, useAppSelector } from "@gno/redux";
-import { selectAccount } from "redux/features/accountSlice";
-import { setFollows } from "redux/features/profileSlice";
+import { broadcastTxCommit, clearLinking, selectQueryParamsTxJsonSigned, setPostToReply, useAppSelector, selectAccount, gnodTxAndRedirectToSign } from "@gno/redux";
+import { followTxAndRedirectToSign, selectProfileAccountName, setFollows, unfollowTxAndRedirectToSign } from "redux/features/profileSlice";
import { useFeed } from "@gno/hooks/use-feed";
import { useUserCache } from "@gno/hooks/use-user-cache";
import ErrorView from "@gno/components/view/account/no-account-view";
@@ -14,7 +13,7 @@ import Layout from "@gno/components/layout";
import { colors } from "@gno/styles/colors";
export default function Page() {
- const { accountName } = useLocalSearchParams<{ accountName: string }>();
+ const accountName = useAppSelector(selectProfileAccountName)
const [loading, setLoading] = useState(undefined);
const [error, setError] = useState(undefined);
@@ -30,6 +29,28 @@ export default function Page() {
const dispatch = useDispatch();
const currentUser = useAppSelector(selectAccount);
+ const txJsonSigned = useAppSelector(selectQueryParamsTxJsonSigned);
+
+ const pathName = usePathname();
+
+ useEffect(() => {
+
+ (async () => {
+ if (txJsonSigned) {
+ console.log("txJsonSigned in [account] page: ", txJsonSigned);
+ const signedTx = decodeURIComponent(txJsonSigned as string)
+ try {
+ await dispatch(broadcastTxCommit(signedTx)).unwrap();
+ } catch (error) {
+ console.error("on broadcastTxCommit", error);
+ }
+
+ dispatch(clearLinking());
+ fetchData();
+ }
+ })();
+
+ }, [txJsonSigned]);
useEffect(() => {
const unsubscribe = navigation.addListener("focus", async () => {
@@ -38,9 +59,12 @@ export default function Page() {
return unsubscribe;
}, [accountName]);
- const fetchData = async () => {
+ const fetchData = useCallback(async () => {
+ console.log("fetchData", accountName);
if (!accountName) return;
+ console.log("fetching data for account: ", currentUser?.bech32);
+
try {
setLoading("Loading account...");
const response = await search.getJsonUserByName(accountName);
@@ -87,7 +111,7 @@ export default function Page() {
} finally {
setLoading(undefined);
}
- };
+ }, [accountName]);
const onPressFollowing = () => {
router.navigate({ pathname: "account/following" });
@@ -98,15 +122,11 @@ export default function Page() {
};
const onPressFollow = async (address: string, callerAddress: Uint8Array) => {
- await search.Follow(address, callerAddress);
-
- fetchData();
+ await dispatch(followTxAndRedirectToSign({ address, callerAddress })).unwrap();
};
const onPressUnfollow = async (address: string, callerAddress: Uint8Array) => {
- await search.Unfollow(address as string, callerAddress);
-
- fetchData();
+ await dispatch(unfollowTxAndRedirectToSign({ address, callerAddress })).unwrap();
};
const onGnod = async (post: Post) => {
@@ -115,18 +135,11 @@ export default function Page() {
if (!currentUser) throw new Error("No active account");
- try {
- await feed.onGnod(post, currentUser.address);
- await fetchData();
- } catch (error) {
- console.error("Error while adding reaction: " + error);
- } finally {
- setLoading(undefined);
- }
+ dispatch(gnodTxAndRedirectToSign({ post, callerAddressBech32: currentUser.bech32, callbackPath: pathName })).unwrap();
};
const onPressPost = async (item: Post) => {
- await dispatch(setPostToReply({ post: item }));
+ await dispatch(setPostToReply(item));
// Posts come from the indexer, the address is a bech32 address.
router.navigate({ pathname: "/post/[post_id]", params: { post_id: item.id, address: String(item.user.address) } });
};
diff --git a/mobile/app/_layout.tsx b/mobile/app/_layout.tsx
index 3b3604df..0f76bd30 100644
--- a/mobile/app/_layout.tsx
+++ b/mobile/app/_layout.tsx
@@ -6,6 +6,7 @@ import { GnoNativeProvider } from "@gnolang/gnonative";
import { IndexerProvider } from "@gno/provider/indexer-provider";
import { NotificationProvider } from "@gno/provider/notification-provider";
import { ReduxProvider } from "redux/redux-provider";
+import { LinkingProvider } from "@gno/provider/linking-provider";
const gnoDefaultConfig = {
// @ts-ignore
@@ -30,17 +31,19 @@ export default function AppLayout() {
-
-
-
-
-
+
+
+
+
+
+
+
diff --git a/mobile/app/home/feed.tsx b/mobile/app/home/feed.tsx
index ea9ad607..486eee5d 100644
--- a/mobile/app/home/feed.tsx
+++ b/mobile/app/home/feed.tsx
@@ -1,12 +1,12 @@
import { ActivityIndicator, FlatList, Platform, StyleSheet, View, Alert as RNAlert, SafeAreaView } from "react-native";
import React, { useEffect, useRef, useState } from "react";
-import { useNavigation, useRouter } from "expo-router";
+import { useNavigation, usePathname, useRouter } from "expo-router";
import { useFeed } from "@gno/hooks/use-feed";
import Layout from "@gno/components/layout";
import useScrollToTop from "@gno/components/utils/useScrollToTopWithOffset";
import Button from "@gno/components/button";
import { Post } from "@gno/types";
-import { selectAccount, setPostToReply, useAppDispatch, useAppSelector } from "@gno/redux";
+import { gnodTxAndRedirectToSign, selectAccount, setPostToReply, useAppDispatch, useAppSelector } from "@gno/redux";
import Alert from "@gno/components/alert";
import { FeedView } from "@gno/components/view";
@@ -22,15 +22,13 @@ export default function Page() {
const dispatch = useAppDispatch();
const account = useAppSelector(selectAccount);
+ const pathName = usePathname();
useScrollToTop(ref, Platform.select({ ios: -150, default: 0 }));
useEffect(() => {
const unsubscribe = navigation.addListener("focus", async () => {
- if (!account) {
- RNAlert.alert("No user found.");
- return;
- }
+ if (!account) return;
setError(undefined);
setIsLoading(true);
try {
@@ -50,23 +48,14 @@ export default function Page() {
router.navigate({ pathname: "/post" });
};
- const onPress = async (item: Post) => {
- await dispatch(setPostToReply({ post: item }));
- router.navigate({ pathname: "/post/[post_id]", params: { post_id: item.id, address: item.user.bech32 } });
+ const onPress = async (p: Post) => {
+ await dispatch(setPostToReply(p));
+ router.navigate({ pathname: "/post/[post_id]" });
};
const onGnod = async (post: Post) => {
- setIsLoading(true);
-
if (!account) throw new Error("No active account");
-
- try {
- await feed.onGnod(post, account.address);
- } catch (error) {
- RNAlert.alert("Error", "Error while adding reaction: " + error);
- } finally {
- setIsLoading(false);
- }
+ dispatch(gnodTxAndRedirectToSign({ post, callerAddressBech32: account.bech32, callbackPath: pathName })).unwrap();
};
if (isLoading)
diff --git a/mobile/app/home/profile.tsx b/mobile/app/home/profile.tsx
index d71c83c5..4fdcd841 100644
--- a/mobile/app/home/profile.tsx
+++ b/mobile/app/home/profile.tsx
@@ -1,8 +1,8 @@
import { Alert, ScrollView, StyleSheet, View } from "react-native";
-import { router, useNavigation } from "expo-router";
+import { router, useNavigation, usePathname } from "expo-router";
import { useEffect, useState } from "react";
import { useGnoNativeContext } from "@gnolang/gnonative";
-import { logedOut, selectAccount, useAppDispatch, useAppSelector } from "@gno/redux";
+import { avatarTxAndRedirectToSign, broadcastTxCommit, clearLinking, logedOut, reloadAvatar, selectAccount, selectQueryParamsTxJsonSigned, useAppDispatch, useAppSelector } from "@gno/redux";
import Button from "@gno/components/button";
import Layout from "@gno/components/layout";
import { LoadingModal } from "@gno/components/loading";
@@ -10,9 +10,10 @@ import { AccountBalance } from "@gno/components/settings";
import Text from "@gno/components/text";
import { useSearch } from "@gno/hooks/use-search";
import { useNotificationContext } from "@gno/provider/notification-provider";
-import { onboarding } from "redux/features/signupSlice";
import AvatarPicker from "@gno/components/avatar/avatar-picker";
import { ProgressViewModal } from "@gno/components/view/progress";
+import { compressImage } from '@gno/utils/file-utils';
+import { useUserCache } from "@gno/hooks/use-user-cache";
export default function Page() {
const [loading, setLoading] = useState(false);
@@ -20,6 +21,7 @@ export default function Page() {
const [chainID, setChainID] = useState("");
const [remote, setRemote] = useState("");
const [followersCount, setFollowersCount] = useState({ n_followers: 0, n_following: 0 });
+ const pathName = usePathname();
const account = useAppSelector(selectAccount);
const { gnonative } = useGnoNativeContext();
@@ -27,6 +29,9 @@ export default function Page() {
const navigation = useNavigation();
const dispatch = useAppDispatch();
const push = useNotificationContext();
+ const txJsonSigned = useAppSelector(selectQueryParamsTxJsonSigned);
+
+ const userCache = useUserCache();
useEffect(() => {
const unsubscribe = navigation.addListener("focus", async () => {
@@ -39,22 +44,6 @@ export default function Page() {
return unsubscribe;
}, [navigation]);
- const onboard = async () => {
- if (!account) {
- console.log("No active account");
- return;
- }
- setLoading(true);
- try {
- await dispatch(onboarding({ account })).unwrap();
- fetchAccountData();
- } catch (error) {
- console.log("Error on onboard", JSON.stringify(error));
- } finally {
- setLoading(false);
- }
- };
-
const onPressNotification = async () => {
if (!account) {
throw new Error("No active account");
@@ -101,13 +90,50 @@ export default function Page() {
dispatch(logedOut());
};
+ const onAvatarChanged = async (imagePath: string, mimeType?: string) => {
+ const imageCompressed = await compressImage(imagePath)
+ if (!imageCompressed || !mimeType || !imageCompressed.base64) {
+ console.log("Error compressing image or missing data");
+ return;
+ }
+
+ if (!account) throw new Error("No account found");
+ await dispatch(avatarTxAndRedirectToSign({ mimeType, base64: imageCompressed.base64, callerAddressBech32: account.bech32, callbackPath: pathName })).unwrap();
+ }
+
+ useEffect(() => {
+
+ (async () => {
+ if (txJsonSigned) {
+
+ console.log("txJsonSigned: ", txJsonSigned);
+
+ const signedTx = decodeURIComponent(txJsonSigned as string)
+ try {
+ await dispatch(broadcastTxCommit(signedTx)).unwrap();
+ } catch (error) {
+ console.error("on broadcastTxCommit", error);
+ }
+
+ dispatch(clearLinking());
+ userCache.invalidateCache();
+
+ setTimeout(() => {
+ console.log("reloading avatar");
+ dispatch(reloadAvatar());
+ }, 500);
+ }
+ })();
+
+ }, [txJsonSigned]);
+
return (
<>
-
+
<>
@@ -124,7 +150,6 @@ export default function Page() {
setModalVisible(false)} />
setModalVisible(true)} variant="primary" />
-
([]);
const account = useAppSelector(selectAccount);
+ const dispatch = useAppDispatch();
useEffect(() => {
(async function () {
@@ -24,8 +25,9 @@ export default function Search() {
})();
}, [params.q]);
- const onPress = (accountName: string) => {
- router.navigate({ pathname: "account", params: { accountName } });
+ const onPress = async (accountName: string) => {
+ await dispatch(setProfileAccountName(accountName));
+ router.navigate({ pathname: "account" });
};
return (
diff --git a/mobile/app/index.tsx b/mobile/app/index.tsx
index 55f99091..ba8f88b9 100644
--- a/mobile/app/index.tsx
+++ b/mobile/app/index.tsx
@@ -1,83 +1,35 @@
-import { useEffect, useState } from "react";
-import { ScrollView, View } from "react-native";
-import { useNavigation, useRouter } from "expo-router";
+import { View } from "react-native";
import Button from "@gno/components/button";
import Layout from "@gno/components/layout";
-import SideMenuAccountList from "@gno/components/list/account/account-list";
-import ReenterPassword from "@gno/components/modal/reenter-password";
import Ruller from "@gno/components/row/Ruller";
import Text from "@gno/components/text";
-import { loggedIn, useAppDispatch } from "@gno/redux";
-import { KeyInfo, useGnoNativeContext } from "@gnolang/gnonative";
+import { clearLinking, loggedIn, requestLoginForGnokeyMobile, selectBech32AddressSelected, useAppDispatch, useAppSelector } from "@gno/redux";
import Spacer from "@gno/components/spacer";
import * as Application from "expo-application";
+import { useEffect } from "react";
+import { useRouter } from "expo-router";
export default function Root() {
- const route = useRouter();
-
- const [accounts, setAccounts] = useState([]);
- const [loading, setLoading] = useState(undefined);
- const [reenterPassword, setReenterPassword] = useState(undefined);
-
- const { gnonative } = useGnoNativeContext();
- const navigation = useNavigation();
const dispatch = useAppDispatch();
+ const route = useRouter();
+ const bech32AddressSelected = useAppSelector(selectBech32AddressSelected)
const appVersion = Application.nativeApplicationVersion;
useEffect(() => {
- const unsubscribe = navigation.addListener("focus", async () => {
- try {
- setLoading("Loading accounts...");
-
- const response = await gnonative.listKeyInfo();
- setAccounts(response);
- } catch (error: unknown | Error) {
- console.error(error);
- } finally {
- setLoading(undefined);
- }
- });
- return unsubscribe;
- }, [navigation]);
-
- const onChangeAccountHandler = async (keyInfo: KeyInfo) => {
- try {
- setLoading("Changing account...");
- const response = await gnonative.activateAccount(keyInfo.name);
-
- setLoading(undefined);
-
- if (!response.hasPassword) {
- setReenterPassword(keyInfo);
- return;
- }
-
- await dispatch(loggedIn({ keyInfo }));
+ console.log("bech32AddressSelected on index", bech32AddressSelected);
+ if (bech32AddressSelected) {
+ dispatch(loggedIn({ bech32: bech32AddressSelected as string }));
+ dispatch(clearLinking());
setTimeout(() => route.replace("/home"), 500);
- } catch (error: unknown | Error) {
- setLoading(error?.toString());
- console.log(error);
}
- };
+ }, [bech32AddressSelected]);
- const onCloseReenterPassword = async (sucess: boolean) => {
- if (sucess && reenterPassword) {
- await dispatch(loggedIn({ keyInfo: reenterPassword }));
- setTimeout(() => route.replace("/home"), 500);
- }
- setReenterPassword(undefined);
- };
- if (loading) {
- return (
-
-
- {loading}
-
-
- );
- }
+
+ const signinUsingGnokey = async () => {
+ await dispatch(requestLoginForGnokeyMobile()).unwrap();
+ };
return (
<>
@@ -90,25 +42,15 @@ export default function Root() {
v{appVersion}
-
- {accounts && accounts.length > 0 && (
- <>
- Please, select one of the existing accounts to start:
-
-
- >
- )}
-
-
+
+ {/* Hero copy */}
+
- Or create a new account:
-
+ Sign in using Gnokey Mobile:
+
- {reenterPassword ? (
-
- ) : null}
>
);
}
diff --git a/mobile/app/post/[post_id].tsx b/mobile/app/post/[post_id].tsx
index 918f59a4..142956ed 100644
--- a/mobile/app/post/[post_id].tsx
+++ b/mobile/app/post/[post_id].tsx
@@ -1,15 +1,14 @@
import { useEffect, useState } from "react";
-import { useLocalSearchParams, useRouter } from "expo-router";
+import { useNavigation, usePathname, useRouter } from "expo-router";
import Text from "@gno/components/text";
-import { selectAccount, selectPostToReply, useAppSelector } from "@gno/redux";
+import { broadcastTxCommit, clearLinking, gnodTxAndRedirectToSign, replyTxAndRedirectToSign, selectAccount, selectPostToReply, selectQueryParamsTxJsonSigned, useAppDispatch, useAppSelector } from "@gno/redux";
import Layout from "@gno/components/layout";
import TextInput from "@gno/components/textinput";
import Button from "@gno/components/button";
import Spacer from "@gno/components/spacer";
import Alert from "@gno/components/alert";
-import { useGnoNativeContext } from "@gnolang/gnonative";
import { PostRow } from "@gno/components/feed/post-row";
-import { FlatList, KeyboardAvoidingView, View, Alert as RNAlert } from "react-native";
+import { FlatList, KeyboardAvoidingView, View } from "react-native";
import { Post } from "@gno/types";
import { useFeed } from "@gno/hooks/use-feed";
@@ -19,27 +18,56 @@ function Page() {
const [loading, setLoading] = useState(undefined);
const [posting, setPosting] = useState(false);
const [thread, setThread] = useState([]);
+
const post = useAppSelector(selectPostToReply);
+ const txJsonSigned = useAppSelector(selectQueryParamsTxJsonSigned);
+ const account = useAppSelector(selectAccount);
+ const navigation = useNavigation();
const feed = useFeed();
const router = useRouter();
- const { gnonative } = useGnoNativeContext();
- const account = useAppSelector(selectAccount);
- const params = useLocalSearchParams();
- const { post_id, address } = params;
+ const pathName = usePathname();
+
+ const dispatch = useAppDispatch();
+
+ useEffect(() => {
+ const unsubscribe = navigation.addListener("focus", async () => {
+ await fetchData();
+ });
+ return unsubscribe;
+ }, [navigation]);
useEffect(() => {
fetchData();
- }, [post_id]);
+ }, [post]);
+
+ useEffect(() => {
+
+ (async () => {
+ if (txJsonSigned) {
+ console.log("txJsonSigned in [post_id] screen:", txJsonSigned);
+ const signedTx = decodeURIComponent(txJsonSigned as string)
+ try {
+ dispatch(clearLinking());
+ await dispatch(broadcastTxCommit(signedTx)).unwrap();
+ } catch (error) {
+ console.error("on broadcastTxCommit", error);
+ } finally {
+ fetchData();
+ }
+ }
+ })();
+
+ }, [txJsonSigned]);
const fetchData = async () => {
if (!post) return;
- console.log("fetching post: ", post_id, address);
+ console.log("fetching post: ", post.user.address);
setLoading("Loading post...");
try {
- const thread = await feed.fetchThread(address as string, Number(post_id));
+ const thread = await feed.fetchThread(String(post.user.address), Number(post.id));
setThread(thread.data);
} catch (error) {
console.error("failed on [post_id].tsx screen", error);
@@ -59,15 +87,7 @@ function Page() {
if (!account) throw new Error("No active account"); // never happens, but just in case
try {
- const gasFee = "1000000ugnot";
- const gasWanted = 10000000;
-
- // Post objects comes from the indexer, address is a bech32 address
- const args: Array = [String(post.user.address), String(post.id), String(post.id), replyContent];
- for await (const response of await gnonative.call("gno.land/r/berty/social", "PostReply", args, gasFee, gasWanted, account.address)) {
- console.log("response ono post screen: ", response);
- }
-
+ await dispatch(replyTxAndRedirectToSign({ post, replyContent, callerAddressBech32: account.bech32, callbackPath: pathName })).unwrap();
setReplyContent("");
await fetchData();
} catch (error) {
@@ -83,22 +103,11 @@ function Page() {
};
const onGnod = async (post: Post) => {
- console.log("gnodding post: ", post);
- setLoading("Gnoding...");
-
if (!account) throw new Error("No active account");
-
- try {
- await feed.onGnod(post, account.address);
- await fetchData();
- } catch (error) {
- RNAlert.alert("Error", "Error while adding reaction: " + error);
- } finally {
- setLoading(undefined);
- }
+ dispatch(gnodTxAndRedirectToSign({ post, callerAddressBech32: account.bech32, callbackPath: pathName }))
};
- if (!post) {
+ if (!post || !post.user) {
return (
diff --git a/mobile/app/post/index.tsx b/mobile/app/post/index.tsx
index 7f517d84..754aaffa 100644
--- a/mobile/app/post/index.tsx
+++ b/mobile/app/post/index.tsx
@@ -3,66 +3,59 @@ import Layout from "@gno/components/layout";
import Spacer from "@gno/components/spacer";
import Text from "@gno/components/text";
import TextInput from "@gno/components/textinput";
-import { useGnoNativeContext } from "@gnolang/gnonative";
import { Stack, useNavigation, useRouter } from "expo-router";
import { useEffect, useState } from "react";
import { KeyboardAvoidingView, Platform } from "react-native";
-import { addProgress } from "redux/features/signupSlice";
-import { selectAccount, useAppDispatch, useAppSelector } from "@gno/redux";
+import { broadcastTxCommit, clearLinking, postTxAndRedirectToSign, selectAccount, selectQueryParamsTxJsonSigned, useAppDispatch, useAppSelector } from "@gno/redux";
export default function Search() {
const [postContent, setPostContent] = useState("");
const [error, setError] = useState(undefined);
const [loading, setLoading] = useState(false);
- const { gnonative } = useGnoNativeContext();
const navigation = useNavigation();
const router = useRouter();
const dispatch = useAppDispatch();
const account = useAppSelector(selectAccount);
+ const txJsonSigned = useAppSelector(selectQueryParamsTxJsonSigned);
+
+ // hook to handle the signed tx from the Gnokey and broadcast it
+ useEffect(() => {
+ const handleSignedTx = async () => {
+ if (txJsonSigned) {
+ const signedTx = decodeURIComponent(txJsonSigned as string);
+ console.log("signedTx: ", signedTx);
+
+ try {
+ setLoading(true);
+ await dispatch(clearLinking())
+ await dispatch(broadcastTxCommit(signedTx))
+ setTimeout(() => {
+ router.push("home");
+ }, 3000);
+ } catch (error) {
+ console.error("on broadcastTxCommit", error);
+ setError("" + error);
+ }
+ }
+ };
+ handleSignedTx();
+ }, [txJsonSigned]);
+
useEffect(() => {
const unsubscribe = navigation.addListener("focus", async () => {
setPostContent("");
- try {
- if (!account) throw new Error("No active account");
- } catch (error: unknown | Error) {
- console.log(error);
- }
+ setLoading(false);
+ if (!account) throw new Error("No active account");
});
return unsubscribe;
}, [navigation]);
- const onPost = async () => {
- setLoading(true);
- setError(undefined);
- dispatch(addProgress(`posting a message.`))
-
- if (!account) throw new Error("No active account"); // never happens, but just in case
-
- try {
- const gasFee = "1000000ugnot";
- const gasWanted = 10000000;
- const args: Array = [postContent];
- for await (const response of await gnonative.call("gno.land/r/berty/social", "PostMessage", args, gasFee, gasWanted, account.address)) {
- console.log("response ono post screen: ", response);
- }
- setPostContent("");
-
- // delay 3s to wait for the transaction to be mined
- // TODO: replace with a better way to wait for the transaction to be mined
- await new Promise((resolve) => setTimeout(resolve, 3000));
-
- dispatch(addProgress(`done, redirecting to home page.`))
- router.push("home");
- } catch (error) {
- dispatch(addProgress(`error on posting a message: ` + JSON.stringify(error)))
- console.error("on post screen", error);
- setError("" + error);
- } finally {
- setLoading(false);
- }
- };
+ const onPressPost = async () => {
+ if (!account || !account.bech32) throw new Error("No active account: " + JSON.stringify(account));
+ await dispatch(postTxAndRedirectToSign({callerAddressBech32: account.bech32, postContent})).unwrap();
+ }
return (
<>
@@ -86,7 +79,7 @@ export default function Search() {
style={{ height: 200 }}
/>
-
+
diff --git a/mobile/app/repost/index.tsx b/mobile/app/repost/index.tsx
index f5c008e2..7156fc5d 100644
--- a/mobile/app/repost/index.tsx
+++ b/mobile/app/repost/index.tsx
@@ -2,7 +2,7 @@ import Button from "@gno/components/button";
import { PostRow } from "@gno/components/feed/post-row";
import Layout from "@gno/components/layout";
import TextInput from "@gno/components/textinput";
-import { selectAccount, selectPostToReply, useAppSelector } from "@gno/redux";
+import { repostTxAndRedirectToSign, selectAccount, selectPostToReply, useAppDispatch, useAppSelector } from "@gno/redux";
import { useGnoNativeContext } from "@gnolang/gnonative";
import { useRouter } from "expo-router";
import { useState } from "react";
@@ -17,6 +17,7 @@ export default function Page() {
const [loading, setLoading] = useState(false);
const account = useAppSelector(selectAccount);
+ const dispatch = useAppDispatch();
const onPressRepost = async () => {
if (!post) return;
@@ -25,15 +26,8 @@ export default function Page() {
setLoading(true);
try {
- const gasFee = "1000000ugnot";
- const gasWanted = 10000000;
- // post.user.address is in fact a bech32 address
- const args: Array = [String(post.user.address), String(post.id), replyContent];
- for await (const response of await gnonative.call("gno.land/r/berty/social", "RepostThread", args, gasFee, gasWanted, account.address)) {
- console.log("response ono post screen: ", response);
- }
+ await dispatch(repostTxAndRedirectToSign({ post, replyContent, callerAddressBech32: account.bech32 })).unwrap();
- // delay 3s to wait for the transaction to be mined
// TODO: replace with a better way to wait for the transaction to be mined
await new Promise((resolve) => setTimeout(resolve, 6000));
diff --git a/mobile/app/sign-up.tsx b/mobile/app/sign-up.tsx
deleted file mode 100644
index fb59b803..00000000
--- a/mobile/app/sign-up.tsx
+++ /dev/null
@@ -1,224 +0,0 @@
-import {
- StyleSheet,
- View,
- Button as RNButton,
- ScrollView,
- TextInput as RNTextInput,
- Alert as RNAlert,
- TouchableOpacity,
-} from "react-native";
-import React, { useEffect, useRef, useState } from "react";
-import { router, useNavigation } from "expo-router";
-import TextInput from "components/textinput";
-import Button from "components/button";
-import Spacer from "components/spacer";
-import * as Clipboard from "expo-clipboard";
-import { loggedIn, useAppDispatch, useAppSelector } from "@gno/redux";
-import Alert from "@gno/components/alert";
-import Layout from "@gno/components/layout";
-import { useGnoNativeContext } from "@gnolang/gnonative";
-import {
- SignUpState,
- clearSignUpState,
- existingAccountSelector,
- newAccountSelector,
- onboarding,
- signUp,
- signUpStateSelector,
-} from "redux/features/signupSlice";
-import { ProgressViewModal } from "@gno/components/view/progress";
-import Text from "@gno/components/text";
-import { MaterialIcons } from "@expo/vector-icons";
-
-export default function Page() {
- const [name, setName] = useState("");
- const [password, setPassword] = useState("");
- const [phrase, setPhrase] = useState("");
- const [error, setError] = useState(undefined);
- const [modalVisible, setModalVisible] = useState(false);
- const [confirmPassword, setConfirmPassword] = useState("");
- const [loading, setLoading] = useState(false);
- const inputRef = useRef(null);
-
- const navigation = useNavigation();
- const { gnonative } = useGnoNativeContext();
- const dispatch = useAppDispatch();
- const signUpState = useAppSelector(signUpStateSelector);
- const newAccount = useAppSelector(newAccountSelector);
- const existingAccount = useAppSelector(existingAccountSelector);
-
- useEffect(() => {
- const unsubscribe = navigation.addListener("focus", async () => {
- setName("");
- setPassword("");
- setConfirmPassword("");
- setError(undefined);
- dispatch(clearSignUpState());
- inputRef.current?.focus();
- try {
- setPhrase(await gnonative.generateRecoveryPhrase());
- } catch (error) {
- console.log(error);
- }
- });
- return unsubscribe;
- }, [navigation]);
-
- useEffect(() => {
- (async () => {
- console.log("signUpState ->", signUpState);
-
- if (signUpState === SignUpState.user_exists_on_blockchain_and_local_storage) {
- setError(
- "This name is already registered on the blockchain and on this device. Please choose another name or press Back for a normal sign in."
- );
- return;
- }
- if (signUpState === SignUpState.user_already_exists_on_blockchain) {
- setError("This name is already registered on the blockchain. Please, choose another name.");
- return;
- }
- if (signUpState === SignUpState.user_already_exists_on_blockchain_under_different_name) {
- setError(
- "This account is already registered on the blockchain under a different name. Please press Back and sign up again with another Seed Phrase, or for a normal sign in with a different account if available."
- );
- return;
- }
- if (signUpState === SignUpState.user_exists_only_on_local_storage) {
- setError(
- "This name is already registered locally on this device but NOT on chain. If you want to register your account on the Gno Blockchain, please press Create again. Your seed phrase will be the same."
- );
- return;
- }
- if (signUpState === SignUpState.user_exists_under_differente_key) {
- setError(
- "This name is already registered locally and on the blockchain under a different key. Please choose another name."
- );
- return;
- }
- if (signUpState === SignUpState.account_created && newAccount) {
- router.push("/home");
- }
- })();
- }, [signUpState, newAccount]);
-
- const copyToClipboard = async () => {
- await Clipboard.setStringAsync(phrase || "");
- };
-
- const onCreate = async () => {
- dispatch(clearSignUpState());
- setError(undefined);
- if (!name || !password) {
- setError("Please fill out all fields");
- return;
- }
-
- // Use the same regex and error message as r/demo/users
- if (!name.match(/^[a-z]+[_a-z0-9]{5,16}$/)) {
- setError("Account name must be at least 6 characters, lowercase alphanumeric with underscore");
- return;
- }
-
- if (password !== confirmPassword) {
- setError("Passwords do not match.");
- return;
- }
-
- if (signUpState === SignUpState.user_exists_only_on_local_storage && existingAccount) {
- await gnonative.activateAccount(name);
- await gnonative.setPassword(password, existingAccount.address);
- await dispatch(onboarding({ account: existingAccount })).unwrap();
- return;
- }
-
- try {
- setLoading(true);
- await dispatch(signUp({ name, password, phrase })).unwrap();
- } catch (error) {
- RNAlert.alert("Error", "" + error);
- setError("" + error);
- console.log(error);
- } finally {
- setLoading(false);
- }
- };
-
- return (
-
-
-
-
- Create your account
-
-
-
-
-
-
-
- Your seed phrase:
-
- {phrase}
-
-
-
-
-
-
- router.back()} variant="secondary" disabled={loading} />
-
-
-
-
- setModalVisible(true)} style={{ flexDirection: "row", alignItems: "center" }}>
- Show Progress
-
-
-
-
- setModalVisible(false)} />
-
-
- );
-}
-
-const styles = StyleSheet.create({
- container: {
- flex: 1,
- alignItems: "center",
- padding: 24,
- },
- main: {
- flex: 1,
- justifyContent: "center",
- maxWidth: 960,
- marginHorizontal: "auto",
- },
- title: {
- fontSize: 64,
- fontWeight: "bold",
- },
- subtitle: {
- fontSize: 36,
- color: "#38434D",
- },
- footer: {
- flex: 1,
- justifyContent: "center",
- alignItems: "center",
- },
-});
diff --git a/mobile/components/auth/guard.tsx b/mobile/components/auth/guard.tsx
index 22cbae6d..bdeca6ae 100644
--- a/mobile/components/auth/guard.tsx
+++ b/mobile/components/auth/guard.tsx
@@ -15,9 +15,12 @@ function useProtectedRoute(user: User | undefined) {
const router = useRouter();
const [segment] = useSegments() as [SharedSegment];
+ const unauthSegments = ["sign-up", "sign-in"];
+
React.useEffect(() => {
- const inAuthGroup = segments.length == 0 || segments[0] === "sign-up" || segments[0] == "sign-in";
+ const inAuthGroup = segments.length == 0 || unauthSegments.includes(segments[0]);
+ console.log("inAuthGroup", inAuthGroup, segments);
// If the user is not signed in and the initial segment is not anything in the auth group.
if (!user && !inAuthGroup) {
router.replace("/");
diff --git a/mobile/components/avatar/avatar-picker.tsx b/mobile/components/avatar/avatar-picker.tsx
index 239ff83f..4dd4a8df 100644
--- a/mobile/components/avatar/avatar-picker.tsx
+++ b/mobile/components/avatar/avatar-picker.tsx
@@ -1,17 +1,18 @@
import React, { useState } from 'react';
import { TouchableOpacity } from 'react-native';
import * as ImagePicker from 'expo-image-picker';
-import { compressImage } from '@gno/utils/file-utils';
-import { reloadAvatar, saveAvatar, selectAvatar, useAppDispatch, useAppSelector } from "@gno/redux";
+import { selectAvatar, useAppSelector } from "@gno/redux";
import Avatar from './avatar';
-const AvatarPicker: React.FC = () => {
+interface Props {
+ onChanged: (imagePath: string, mimeType?: string) => void;
+}
+
+const AvatarPicker: React.FC = ({onChanged}) => {
const [base64Image, setBase64Image] = useState(null);
const avatarBase64 = useAppSelector(selectAvatar);
- const dispatch = useAppDispatch();
-
const pickImage = async () => {
let result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.Images,
@@ -25,14 +26,7 @@ const AvatarPicker: React.FC = () => {
const imagePath = result.assets[0].uri;
const mimeType = result.assets[0].mimeType;
- const imageCompressed = await compressImage(imagePath)
- if (!imageCompressed || !mimeType || !imageCompressed.base64) {
- console.log("Error compressing image or missing data");
- return;
- }
- await dispatch(saveAvatar({ mimeType, base64: imageCompressed.base64 })).unwrap();
-
- await dispatch(reloadAvatar()).unwrap();
+ onChanged(imagePath, mimeType);
}
}
diff --git a/mobile/components/feed/post-row.tsx b/mobile/components/feed/post-row.tsx
index 03984fe8..2d28f10b 100644
--- a/mobile/components/feed/post-row.tsx
+++ b/mobile/components/feed/post-row.tsx
@@ -5,7 +5,7 @@ import Text from "@gno/components/text";
import RepliesLabel from "./replies-label";
import TimeStampLabel from "./timestamp-label";
import RepostButton from "./repost-button";
-import { setPostToReply, useAppDispatch } from "@gno/redux";
+import { setPostToReply, useAppDispatch, setProfileAccountName } from "@gno/redux";
import { useRouter } from "expo-router";
import RepostLabel from "./repost-label";
import { RepostRow } from "./repost-row";
@@ -26,13 +26,14 @@ export function PostRow({ post, onPress = func, onGnod = func, showFooter = true
const dispatch = useAppDispatch();
const isRepost = post?.repost_parent;
- const onPressRepost = async (item: Post) => {
- await dispatch(setPostToReply({ post: item }));
+ const onPressRepost = async (p: Post) => {
+ await dispatch(setPostToReply(p));
router.navigate({ pathname: "/repost" });
};
const nativgateToAccount = async (accountName: string) => {
- router.navigate({ pathname: "account", params: { accountName } });
+ await dispatch(setProfileAccountName(accountName));
+ router.navigate({ pathname: "account" });
};
if (!post) {
diff --git a/mobile/components/feed/repost-row.tsx b/mobile/components/feed/repost-row.tsx
index baa73854..33222db6 100644
--- a/mobile/components/feed/repost-row.tsx
+++ b/mobile/components/feed/repost-row.tsx
@@ -5,7 +5,7 @@ import Text from "@gno/components/text";
import RepliesLabel from "./replies-label";
import TimeStampLabel from "./timestamp-label";
import RepostButton from "./repost-button";
-import { setPostToReply, useAppDispatch } from "@gno/redux";
+import { setPostToReply, useAppDispatch, setProfileAccountName } from "@gno/redux";
import { useRouter } from "expo-router";
import RepostLabel from "./repost-label";
@@ -21,13 +21,14 @@ export function RepostRow({ post, onPress = func, showFooter = true }: FeedProps
const router = useRouter();
const dispatch = useAppDispatch();
- const onPressRepost = async (item: Post) => {
- await dispatch(setPostToReply({ post: item }));
+ const onPressRepost = async (p: Post) => {
+ await dispatch(setPostToReply(p));
router.navigate({ pathname: "/repost" });
};
const onPressName = async () => {
- router.navigate({ pathname: "account", params: { accountName: post?.user.name } });
+ await dispatch(setProfileAccountName(post?.user.name));
+ router.navigate({ pathname: "account" });
};
if (!post) {
diff --git a/mobile/components/view/account/index.tsx b/mobile/components/view/account/index.tsx
index bad769f1..dac94f30 100644
--- a/mobile/components/view/account/index.tsx
+++ b/mobile/components/view/account/index.tsx
@@ -40,7 +40,7 @@ function AccountView(props: Props) {
} = props;
const accountName = user.name;
- const isFollowed = useMemo(() => followers.find((f) => f.address.toString() === currentUser.address.toString()) != null, [user, followers]);
+ const isFollowed = useMemo(() => followers.find((f) => f.address.toString() === currentUser.bech32) != null, [user, followers]);
const avarUri = user.avatar ? user.avatar : "https://www.gravatar.com/avatar/tmp";
@@ -54,14 +54,14 @@ function AccountView(props: Props) {
{isFollowed ? (
onPressUnfollow(user.address.toString(), callerAddress)}
+ onPress={() => onPressUnfollow(user.bech32, callerAddress)}
variant="primary"
title="Unfollow"
style={{ width: 100 }}
/>
) : (
onPressFollow(user.address.toString(), callerAddress)}
+ onPress={() => onPressFollow(user.bech32, callerAddress)}
variant="primary"
title="Follow"
style={{ width: 100 }}
diff --git a/mobile/components/view/progress/modal.tsx b/mobile/components/view/progress/modal.tsx
index c8d93745..2a971b56 100644
--- a/mobile/components/view/progress/modal.tsx
+++ b/mobile/components/view/progress/modal.tsx
@@ -1,9 +1,7 @@
import React from "react";
import { View, Modal, StyleSheet, FlatList, TouchableOpacity, Share } from "react-native";
-import { useAppDispatch, useAppSelector } from "@gno/redux";
import { colors } from "@gno/styles/colors";
import Layout from "@gno/components/layout";
-import { clearProgress, selectProgress } from "redux/features/signupSlice";
import Text from "@gno/components/text";
import { EvilIcons } from "@expo/vector-icons";
@@ -12,12 +10,13 @@ interface Props {
onRequestClose: () => void;
}
+// TODO: reimplment this
const ProgressViewModal: React.FC = ({ visible, onRequestClose }) => {
- const dispatch = useAppDispatch();
- const progress = useAppSelector(selectProgress);
+ //const dispatch = useAppDispatch();
+ const progress = [""]
const clear = async () => {
- await dispatch(clearProgress());
+ //await dispatch(clearProgress());
};
const share = async () => {
diff --git a/mobile/package-lock.json b/mobile/package-lock.json
index 9683057f..e305a31b 100644
--- a/mobile/package-lock.json
+++ b/mobile/package-lock.json
@@ -15,7 +15,7 @@
"@bufbuild/protobuf": "^1.6.0",
"@connectrpc/connect": "^1.2.1",
"@connectrpc/connect-web": "^1.2.1",
- "@gnolang/gnonative": "^1.8.1",
+ "@gnolang/gnonative": "3.0.0",
"@reduxjs/toolkit": "^2.1.0",
"base-64": "^1.0.0",
"date-fns": "^3.6.0",
@@ -2354,29 +2354,22 @@
}
},
"node_modules/@buf/gnolang_gnonative.bufbuild_es": {
- "version": "1.10.0-20240917104336-5a54dc3eb1e1.1",
- "resolved": "https://buf.build/gen/npm/v1/@buf/gnolang_gnonative.bufbuild_es/-/gnolang_gnonative.bufbuild_es-1.10.0-20240917104336-5a54dc3eb1e1.1.tgz",
+ "version": "1.7.2-20240919093120-8280c8c152be.2",
+ "resolved": "https://buf.build/gen/npm/v1/@buf/gnolang_gnonative.bufbuild_es/-/gnolang_gnonative.bufbuild_es-1.7.2-20240919093120-8280c8c152be.2.tgz",
"peerDependencies": {
- "@bufbuild/protobuf": "^1.10.0"
+ "@bufbuild/protobuf": "^1.7.2"
}
},
"node_modules/@buf/gnolang_gnonative.connectrpc_es": {
- "version": "1.4.0-20240917104336-5a54dc3eb1e1.3",
- "resolved": "https://buf.build/gen/npm/v1/@buf/gnolang_gnonative.connectrpc_es/-/gnolang_gnonative.connectrpc_es-1.4.0-20240917104336-5a54dc3eb1e1.3.tgz",
+ "version": "1.4.0-20240919093120-8280c8c152be.3",
+ "resolved": "https://buf.build/gen/npm/v1/@buf/gnolang_gnonative.connectrpc_es/-/gnolang_gnonative.connectrpc_es-1.4.0-20240919093120-8280c8c152be.3.tgz",
"dependencies": {
- "@buf/gnolang_gnonative.bufbuild_es": "1.7.2-20240917104336-5a54dc3eb1e1.2"
+ "@buf/gnolang_gnonative.bufbuild_es": "1.7.2-20240919093120-8280c8c152be.2"
},
"peerDependencies": {
"@connectrpc/connect": "^1.4.0"
}
},
- "node_modules/@buf/gnolang_gnonative.connectrpc_es/node_modules/@buf/gnolang_gnonative.bufbuild_es": {
- "version": "1.7.2-20240917104336-5a54dc3eb1e1.2",
- "resolved": "https://buf.build/gen/npm/v1/@buf/gnolang_gnonative.bufbuild_es/-/gnolang_gnonative.bufbuild_es-1.7.2-20240917104336-5a54dc3eb1e1.2.tgz",
- "peerDependencies": {
- "@bufbuild/protobuf": "^1.7.2"
- }
- },
"node_modules/@bufbuild/protobuf": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-1.10.0.tgz",
@@ -3523,12 +3516,12 @@
}
},
"node_modules/@gnolang/gnonative": {
- "version": "1.8.1",
- "resolved": "https://registry.npmjs.org/@gnolang/gnonative/-/gnonative-1.8.1.tgz",
- "integrity": "sha512-eAW61aqm7fy2oyc5GjZUJzVyd+gKLQjp69gMINrM2OubMciiBfTziLuyha0tG0LbIaJkeCRnVjp5KyG04+NvEA==",
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/@gnolang/gnonative/-/gnonative-3.0.0.tgz",
+ "integrity": "sha512-2vtfdEp0pCtDdIwupW8KaT948WYFLXT3Re+llEl3XaCmsVTIUKI+/Aicr6u1A5xShbw9FTPsPoBT30hgbk7qaQ==",
"dependencies": {
- "@buf/gnolang_gnonative.bufbuild_es": "^1.10.0-20240722123703-a858ea7341d6.1",
- "@buf/gnolang_gnonative.connectrpc_es": "^1.4.0-20240722123703-a858ea7341d6.3",
+ "@buf/gnolang_gnonative.bufbuild_es": "^1.7.2-20240919093120-8280c8c152be.2",
+ "@buf/gnolang_gnonative.connectrpc_es": "^1.4.0-20240919093120-8280c8c152be.3",
"@bufbuild/protobuf": "^1.8.0",
"@connectrpc/connect": "^1.4.0",
"@connectrpc/connect-web": "^1.4.0",
diff --git a/mobile/package.json b/mobile/package.json
index b52133d4..0ff2e8a7 100644
--- a/mobile/package.json
+++ b/mobile/package.json
@@ -18,7 +18,7 @@
"@bufbuild/protobuf": "^1.6.0",
"@connectrpc/connect": "^1.2.1",
"@connectrpc/connect-web": "^1.2.1",
- "@gnolang/gnonative": "^1.8.1",
+ "@gnolang/gnonative": "3.0.0",
"@reduxjs/toolkit": "^2.1.0",
"base-64": "^1.0.0",
"date-fns": "^3.6.0",
diff --git a/mobile/redux/features/accountSlice.ts b/mobile/redux/features/accountSlice.ts
index ec705bb3..1531c1e6 100644
--- a/mobile/redux/features/accountSlice.ts
+++ b/mobile/redux/features/accountSlice.ts
@@ -1,8 +1,8 @@
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
+import { makeCallTx } from "./linkingSlice";
import { User } from "@gno/types";
-import { GnoNativeApi, KeyInfo } from "@gnolang/gnonative";
+import { GnoNativeApi } from "@gnolang/gnonative";
import { ThunkExtra } from "redux/redux-provider";
-import { useUserCache } from "@gno/hooks/use-user-cache";
export interface CounterState {
account?: User;
@@ -13,46 +13,50 @@ const initialState: CounterState = {
};
interface LoginParam {
- keyInfo: KeyInfo;
+ bech32: string;
}
export const loggedIn = createAsyncThunk("account/loggedIn", async (param, thunkAPI) => {
- const { keyInfo } = param;
+ console.log("Logging in", param);
+ const { bech32 } = param;
const gnonative = thunkAPI.extra.gnonative as GnoNativeApi;
- const bech32 = await gnonative.addressToBech32(keyInfo.address);
- const user: User = { bech32, ...keyInfo };
-
- user.avatar = await loadBech32AvatarFromChain(bech32, thunkAPI);
+ const user: User = {
+ name: await getAccountName(bech32, gnonative) || 'Unknown',
+ address: await gnonative.addressFromBech32(bech32),
+ bech32,
+ avatar: await loadBech32AvatarFromChain(bech32, thunkAPI)
+ };
return user;
});
-export const saveAvatar = createAsyncThunk("account/saveAvatar", async (param, thunkAPI) => {
- const { mimeType, base64 } = param;
-
- const gnonative = thunkAPI.extra.gnonative;
- const userCache = thunkAPI.extra.userCache
+async function getAccountName(bech32: string, gnonative: GnoNativeApi) {
+ const accountNameStr = await gnonative.qEval("gno.land/r/demo/users", `GetUserByAddress("${bech32}").Name`);
+ console.log("GetUserByAddress result:", accountNameStr);
+ const accountName = accountNameStr.match(/\("(\w+)"/)?.[1];
+ console.log("GetUserByAddress after regex", accountName);
+ return accountName
+}
- const state = await thunkAPI.getState() as CounterState;
- console.log("statexxx", state);
- // @ts-ignore
- const address = state.account?.account?.address;
+interface AvatarCallTxParams {
+ mimeType: string;
+ base64: string;
+ callerAddressBech32: string;
+ callbackPath: string;
+}
- try {
- const gasFee = "1000000ugnot";
- const gasWanted = 10000000;
+export const avatarTxAndRedirectToSign = createAsyncThunk("account/avatarTxAndRedirectToSign", async (props, thunkAPI) => {
+ const { mimeType, base64, callerAddressBech32, callbackPath } = props;
- const args: Array = ["Avatar", String(`data:${mimeType};base64,` + base64)];
- for await (const response of await gnonative.call("gno.land/r/demo/profile", "SetStringField", args, gasFee, gasWanted, address)) {
- console.log("response on saving avatar: ", response);
- }
+ const gnonative = thunkAPI.extra.gnonative;
- userCache.invalidateCache();
- } catch (error) {
- console.error("on saving avatar", error);
- }
+ const gasFee = "1000000ugnot";
+ const gasWanted = BigInt(10000000);
+ const args: Array = ["Avatar", String(`data:${mimeType};base64,` + base64)];
+ const reason = "Upload a new avatar";
+ await makeCallTx({ packagePath: "gno.land/r/demo/profile", fnc: "SetStringField", args, gasFee, gasWanted, callerAddressBech32, reason, callbackPath }, gnonative);
});
export const reloadAvatar = createAsyncThunk("account/reloadAvatar", async (param, thunkAPI) => {
@@ -92,6 +96,7 @@ export const accountSlice = createSlice({
extraReducers(builder) {
builder.addCase(loggedIn.fulfilled, (state, action) => {
state.account = action.payload;
+ console.log("Logged in", action.payload);
});
builder.addCase(loggedIn.rejected, (_, action) => {
console.error("loggedIn.rejected", action);
diff --git a/mobile/redux/features/index.ts b/mobile/redux/features/index.ts
index 3ce192af..3c29456e 100644
--- a/mobile/redux/features/index.ts
+++ b/mobile/redux/features/index.ts
@@ -1,3 +1,4 @@
export * from "./accountSlice";
export * from "./profileSlice";
export * from "./replySlice";
+export * from "./linkingSlice";
diff --git a/mobile/redux/features/linkingSlice.ts b/mobile/redux/features/linkingSlice.ts
new file mode 100644
index 00000000..7a78170e
--- /dev/null
+++ b/mobile/redux/features/linkingSlice.ts
@@ -0,0 +1,119 @@
+import { Post } from "@gno/types";
+import { GnoNativeApi } from "@gnolang/gnonative";
+import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
+import * as Linking from 'expo-linking';
+import { ThunkExtra } from "redux/redux-provider";
+
+interface State {
+ txJsonSigned: string | undefined;
+ bech32AddressSelected: string | undefined;
+}
+
+const initialState: State = {
+ txJsonSigned: undefined,
+ bech32AddressSelected: undefined,
+};
+
+export const requestLoginForGnokeyMobile = createAsyncThunk("tx/requestLoginForGnokeyMobile", async () => {
+ console.log("requesting login for GnokeyMobile");
+ const callback = encodeURIComponent('tech.berty.dsocial://login-callback');
+ return await Linking.openURL(`land.gno.gnokey://tologin?callback=${callback}`);
+})
+
+type MakeTxAndRedirectParams = {
+ postContent: string,
+ callerAddressBech32: string,
+};
+
+export const postTxAndRedirectToSign = createAsyncThunk("tx/makeCallTxAndRedirectToSign", async (props, thunkAPI) => {
+ const { callerAddressBech32, postContent } = props;
+
+ const fnc = "PostMessage";
+ const args: Array = [postContent];
+ const gasFee = "1000000ugnot";
+ const gasWanted = BigInt(10000000);
+ const reason = "Post a message";
+ const callbackPath = "/post";
+
+ await makeCallTx({ fnc, args, gasFee, gasWanted, callerAddressBech32, reason, callbackPath }, thunkAPI.extra.gnonative);
+})
+
+type MakeCallTxParams = {
+ packagePath?: string,
+ fnc: string,
+ args: string[],
+ gasFee: string,
+ gasWanted: bigint,
+ send?: string,
+ memo?: string,
+ callerAddressBech32: string,
+ reason: string,
+ callbackPath: string,
+};
+
+export const makeCallTx = async (props: MakeCallTxParams, gnonative: GnoNativeApi): Promise => {
+ const { fnc, callerAddressBech32, gasFee, gasWanted, args, packagePath = "gno.land/r/berty/social", reason, callbackPath } = props;
+
+ console.log("making a tx for: ", callerAddressBech32);
+ const address = await gnonative.addressFromBech32(callerAddressBech32);
+
+ const res = await gnonative.makeCallTx(packagePath, fnc, args, gasFee, gasWanted, address)
+
+ const params = [`tx=${encodeURIComponent(res.txJson)}`, `address=${callerAddressBech32}`, `client_name=dSocial`, `reason=${reason}`, `callback=${encodeURIComponent('tech.berty.dsocial:/' + callbackPath)}`];
+ Linking.openURL('land.gno.gnokey://tosign?' + params.join('&'))
+}
+
+export const broadcastTxCommit = createAsyncThunk("tx/broadcastTxCommit", async (signedTx, thunkAPI) => {
+ console.log("broadcasting tx: ", signedTx);
+
+ const gnonative = thunkAPI.extra.gnonative;
+ const res = await gnonative.broadcastTxCommit(signedTx);
+ console.log("broadcasted tx: ", res);
+});
+
+interface GnodCallTxParams {
+ post: Post,
+ callerAddressBech32: string,
+ callbackPath: string,
+}
+
+export const gnodTxAndRedirectToSign = createAsyncThunk("tx/gnodTxAndRedirectToSign", async (props, thunkAPI) => {
+ console.log("gnodding post: ", props.post);
+ const { post, callerAddressBech32, callbackPath } = props;
+
+ const fnc = "AddReaction";
+ const gasFee = "1000000ugnot";
+ const gasWanted = BigInt(2000000);
+ // post.user.address is in fact a bech32 address
+ const args: Array = [String(post.user.address), String(post.id), String(post.id), String("0")];
+ const reason = "Gnoding a message";
+ await makeCallTx({ fnc, args, gasFee, gasWanted, callerAddressBech32, reason, callbackPath }, thunkAPI.extra.gnonative);
+});
+
+/**
+ * Slice to handle linking between the app and the GnokeyMobile app
+ */
+export const linkingSlice = createSlice({
+ name: "linking",
+ initialState,
+ reducers: {
+ setLinkingData: (state, action) => {
+ const queryParams = action.payload.queryParams
+
+ state.bech32AddressSelected = queryParams?.address ? queryParams.address as string : undefined
+ state.txJsonSigned = queryParams?.tx ? queryParams.tx as string : undefined
+ },
+ clearLinking: (state) => {
+ console.log("clearing linking data");
+ state = { ...initialState };
+ }
+ },
+ selectors: {
+ selectQueryParamsTxJsonSigned: (state: State) => state.txJsonSigned as string | undefined,
+ selectBech32AddressSelected: (state: State) => state.bech32AddressSelected as string | undefined,
+ },
+});
+
+export const { clearLinking, setLinkingData } = linkingSlice.actions;
+
+export const { selectQueryParamsTxJsonSigned, selectBech32AddressSelected } = linkingSlice.selectors;
diff --git a/mobile/redux/features/profileSlice.ts b/mobile/redux/features/profileSlice.ts
index 22aef762..f9787048 100644
--- a/mobile/redux/features/profileSlice.ts
+++ b/mobile/redux/features/profileSlice.ts
@@ -1,5 +1,9 @@
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
+import { makeCallTx } from "./linkingSlice";
import { Following } from "@gno/types";
+import { ThunkExtra } from "redux/redux-provider";
+
+const CLIENT_NAME_PARAM = 'client_name=dSocial';
export interface ProfileState {
following: Following[];
@@ -18,6 +22,36 @@ interface FollowsProps {
followers: Following[];
}
+export const followTxAndRedirectToSign = createAsyncThunk("profile/follow", async ({ address, callerAddress }, thunkAPI) => {
+ console.log("Follow user: %s", address);
+ const gnonative = thunkAPI.extra.gnonative;
+
+ const fnc = "Follow";
+ const args: Array = [address];
+ const gasFee = "1000000ugnot";
+ const gasWanted = BigInt(10000000);
+ const callerAddressBech32 = await gnonative.addressToBech32(callerAddress);
+ const reason = "Follow a user";
+ const callbackPath = "/account";
+
+ await makeCallTx({ fnc, args, gasFee, gasWanted, callerAddressBech32, reason, callbackPath }, thunkAPI.extra.gnonative);
+});
+
+export const unfollowTxAndRedirectToSign = createAsyncThunk("profile/follow", async ({ address, callerAddress }, thunkAPI) => {
+ console.log("Follow user: %s", address);
+ const gnonative = thunkAPI.extra.gnonative;
+
+ const fnc = "Unfollow";
+ const args: Array = [address];
+ const gasFee = "1000000ugnot";
+ const gasWanted = BigInt(10000000);
+ const callerAddressBech32 = await gnonative.addressToBech32(callerAddress);
+ const reason = "Unfollow a user";
+ const callbackPath = "/account";
+
+ await makeCallTx({ fnc, args, gasFee, gasWanted, callerAddressBech32, reason, callbackPath }, thunkAPI.extra.gnonative);
+});
+
export const setFollows = createAsyncThunk("profile/setFollows", async ({ following, followers }: FollowsProps, _) => {
return { following, followers };
});
@@ -25,9 +59,13 @@ export const setFollows = createAsyncThunk("profile/setFollows", async ({ follow
export const profileSlice = createSlice({
name: "profile",
initialState,
- reducers: {},
+ reducers: {
+ setProfileAccountName: (state, action) => {
+ state.accountName = action.payload;
+ }
+ },
selectors: {
- selectAccountName: (state) => state.accountName,
+ selectProfileAccountName: (state) => state.accountName,
selectFollowers: (state) => state.followers,
selectFollowing: (state) => state.following,
},
@@ -42,4 +80,6 @@ export const profileSlice = createSlice({
},
});
-export const { selectAccountName, selectFollowers, selectFollowing } = profileSlice.selectors;
+export const { setProfileAccountName } = profileSlice.actions;
+
+export const { selectProfileAccountName, selectFollowers, selectFollowing } = profileSlice.selectors;
diff --git a/mobile/redux/features/replySlice.ts b/mobile/redux/features/replySlice.ts
index 56b42d4d..7d5a6173 100644
--- a/mobile/redux/features/replySlice.ts
+++ b/mobile/redux/features/replySlice.ts
@@ -1,5 +1,7 @@
-import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
+import { createAsyncThunk, createSlice, PayloadAction } from "@reduxjs/toolkit";
+import { makeCallTx } from "./linkingSlice";
import { Post } from "@gno/types";
+import { ThunkExtra } from "redux/redux-provider";
export interface State {
postToReply: Post | undefined;
@@ -9,25 +11,57 @@ const initialState: State = {
postToReply: undefined,
};
-export const setPostToReply = createAsyncThunk("post/reply", async ({ post }: { post: Post }) => {
- return { post };
-});
+interface RepostTxAndRedirectParams {
+ post: Post;
+ replyContent: string;
+ callerAddressBech32: string;
+}
+
+export const repostTxAndRedirectToSign = createAsyncThunk("tx/repostTxAndRedirectToSign", async (props, thunkAPI) => {
+ const { post, replyContent, callerAddressBech32 } = props;
+
+ const fnc = "RepostThread";
+ // post.user.address is in fact a bech32 address
+ const args: Array = [String(post.user.address), String(post.id), replyContent];
+ const gasFee = "1000000ugnot";
+ const gasWanted = BigInt(10000000);
+ const reason = "Repost a message";
+ const callbackPath = "/repost";
+
+ await makeCallTx({ fnc, args, gasFee, gasWanted, callerAddressBech32, reason, callbackPath }, thunkAPI.extra.gnonative);
+})
+
+type ReplytTxAndRedirectParams = {
+ post: Post;
+ replyContent: string;
+ callerAddressBech32: string;
+ callbackPath: string;
+}
+
+export const replyTxAndRedirectToSign = createAsyncThunk("tx/replyTxAndRedirectToSign", async (props, thunkAPI) => {
+ const { post, replyContent, callerAddressBech32, callbackPath } = props;
+
+ const fnc = "PostReply";
+ const gasFee = "1000000ugnot";
+ const gasWanted = BigInt(10000000);
+ const args: Array = [String(post.user.address), String(post.id), String(post.id), replyContent];
+ const reason = "Reply a message";
+
+ await makeCallTx({ fnc, args, gasFee, gasWanted, callerAddressBech32, reason, callbackPath }, thunkAPI.extra.gnonative);
+})
export const replySlice = createSlice({
name: "reply",
initialState,
- reducers: {},
+ reducers: {
+ setPostToReply: (state, action: PayloadAction) => {
+ state.postToReply = action.payload;
+ }
+ },
selectors: {
selectPostToReply: (state) => state.postToReply,
- },
- extraReducers(builder) {
- builder.addCase(setPostToReply.fulfilled, (state, action) => {
- state.postToReply = action.payload.post;
- });
- builder.addCase(setPostToReply.rejected, (state, action) => {
- console.log("Error while replying a post, please, check the logs. %s", action.error.message);
- });
- },
+ }
});
+export const { setPostToReply } = replySlice.actions;
export const { selectPostToReply } = replySlice.selectors;
diff --git a/mobile/redux/features/signupSlice.ts b/mobile/redux/features/signupSlice.ts
deleted file mode 100644
index e745dfaa..00000000
--- a/mobile/redux/features/signupSlice.ts
+++ /dev/null
@@ -1,326 +0,0 @@
-import { PayloadAction, createAsyncThunk, createSlice } from "@reduxjs/toolkit";
-import { GnoNativeApi, KeyInfo } from "@gnolang/gnonative";
-import { ThunkExtra } from "redux/redux-provider";
-import { Alert } from "react-native";
-import { UseNotificationReturnType } from "@gno/provider/notification-provider";
-import { UseSearchReturnType } from "@gno/hooks/use-search";
-import { User } from "@gno/types";
-import { loggedIn } from "@gno/redux";
-
-
-export enum SignUpState {
- user_exists_on_blockchain_and_local_storage = 'user_exists_on_blockchain_and_local_storage',
- user_exists_under_differente_key = 'user_exists_under_differente_key',
- user_exists_only_on_local_storage = 'user_exists_only_on_local_storage',
- user_already_exists_on_blockchain_under_different_name = 'user_already_exists_on_blockchain_under_different_name',
- user_already_exists_on_blockchain = 'user_already_exists_on_blockchain',
- account_created = 'account_created',
-}
-
-export interface CounterState {
- signUpState?: SignUpState
- newAccount?: User;
- existingAccount?: User;
- loading: boolean;
- progress: string[];
-}
-
-const initialState: CounterState = {
- signUpState: undefined,
- newAccount: undefined,
- existingAccount: undefined,
- loading: false,
- progress: [],
-};
-
-interface SignUpParam {
- name: string;
- password: string;
- phrase: string;
-}
-
-type SignUpResponse = { newAccount?: User, existingAccount?: User, state: SignUpState };
-
-/**
- * This thunk checks if the user is already registered on the blockchain and/or local storage.
- * The output is a state that will be used to decide the next step (signUpState).
- *
- * CASE 1.0: The user is: local storage (yes), blockchain (yes), under same name (yes) and address (yes), it will offer to do normal signin or choose new name.
- * CASE 1.1: The user is: local storage (yes), blockchain (yes), under same name (yes) and address (no), it will offer to delete the local storage.
- * CASE 1.2: The user is: local storage (yes), blockchain (no), under same name (---) and address (--), it will offer to delete the local storage.
- *
- * CASE 2.0: The user is: local storage (no), blockchain (yes), under same name (no) and address (yes)
- * CASE 2.1: The user is: local storage (no), blockchain (yes), under same name (no) and address (no)
- *
- * CASE 3.0: The user is: local storage (no), blockchain (no), under same name (---) and address (--), it will proceed to create the account.
- *
- * ref: https://github.com/gnolang/dsocial/issues/72
- */
-export const signUp = createAsyncThunk("user/signUp", async (param, thunkAPI) => {
-
- const { name, password, phrase } = param;
- const gnonative = thunkAPI.extra.gnonative as GnoNativeApi;
- const search = thunkAPI.extra.search;
- const push = thunkAPI.extra.push;
-
- thunkAPI.dispatch(addProgress(`checking if "${name}" is already registered on the blockchain.`))
- const blockchainUser = await checkForUserOnBlockchain(gnonative, search, name, phrase);
- thunkAPI.dispatch(addProgress(`response: "${JSON.stringify(blockchainUser)}"`))
-
- thunkAPI.dispatch(addProgress(`checking if "${name}" is already on local storage`))
- const userOnLocalStorage = await checkForUserOnLocalStorage(gnonative, name);
- thunkAPI.dispatch(addProgress(`response for "${name}": ${JSON.stringify(userOnLocalStorage)}`))
-
- if (userOnLocalStorage) {
- if (blockchainUser) {
- const localAddress = await gnonative.addressToBech32(userOnLocalStorage.address);
- thunkAPI.dispatch(addProgress(`exisging local address "${localAddress}" and blockchain Users Addr "${blockchainUser.address}"`))
-
- if (blockchainUser.address == localAddress) {
- thunkAPI.dispatch(addProgress(`CASE 1.0 SignUpState.user_exists_on_blockchain_and_local_storage`))
- // CASE 1.0: Offer to do normal signin, or choose new name
- return { newAccount: undefined, state: SignUpState.user_exists_on_blockchain_and_local_storage }
-
- }
- else {
- thunkAPI.dispatch(addProgress(`SignUpState.user_exists_under_differente_key`))
- // CASE 1.1: Bad case. Choose new name. (Delete name in keystore?)
- return { newAccount: undefined, state: SignUpState.user_exists_under_differente_key }
- }
- }
- else {
- thunkAPI.dispatch(addProgress(`SignUpState.user_exists_only_on_local_storage`))
- // CASE 1.2: Offer to onboard existing account, replace it, or choose new name
- return { newAccount: undefined, state: SignUpState.user_exists_only_on_local_storage, existingAccount: userOnLocalStorage }
- }
- } else {
- if (blockchainUser) {
- // This name is already registered on the blockchain.
- // CASE 2.0: Offer to rename keystoreInfoByAddr.name to name in keystore (password check), and do signin
- // CASE 2.1: "This name is already registered on the blockchain. Please choose another name."
- thunkAPI.dispatch(addProgress(blockchainUser.state))
- return { newAccount: undefined, state: blockchainUser.state }
- }
-
- // Proceed to create the account.
- // CASE 3.0: Proceed to create the account.
- const newAccount = await gnonative.createAccount(name, phrase, password);
- if (!newAccount) {
- thunkAPI.dispatch(addProgress(`Failed to create account "${name}"`))
- throw new Error(`Failed to create account "${name}"`);
- }
-
- console.log("createAccount response: " + JSON.stringify(newAccount));
-
- await gnonative.activateAccount(name);
- await gnonative.setPassword(password, newAccount.address);
-
- thunkAPI.dispatch(addProgress(`onboarding "${name}"`))
- await onboard(gnonative, push, newAccount);
-
- thunkAPI.dispatch(addProgress(`SignUpState.account_created`))
-
- const bech32 = await gnonative.addressToBech32(newAccount.address);
- const user: User = { bech32, ...newAccount };
-
- await thunkAPI.dispatch(loggedIn({ keyInfo: newAccount })).unwrap();
-
- return { newAccount: user, state: SignUpState.account_created };
- }
-})
-
-export const onboarding = createAsyncThunk("user/onboarding", async (param, thunkAPI) => {
- thunkAPI.dispatch(addProgress(`onboarding "${param.account.name}"`))
-
- const push = thunkAPI.extra.push;
-
- const { account } = param;
- const gnonative = thunkAPI.extra.gnonative as GnoNativeApi;
- await onboard(gnonative, push, account);
-
- thunkAPI.dispatch(addProgress(`SignUpState.account_created`))
- return { newAccount: account, state: SignUpState.account_created };
-})
-
-const checkForUserOnLocalStorage = async (gnonative: GnoNativeApi, name: string): Promise => {
- let userOnLocalStorage: User | undefined = undefined;
- try {
- const keyInfo = await gnonative.getKeyInfoByNameOrAddress(name);
- if (keyInfo) {
- userOnLocalStorage = {...keyInfo, bech32: await gnonative.addressToBech32(keyInfo.address)};
- }
-
- } catch (e) {
- // TODO: Check for error other than ErrCryptoKeyNotFound(#151)
- return undefined;
- }
- return userOnLocalStorage
-}
-
-const checkForUserOnBlockchain = async (gnonative: GnoNativeApi, search: UseSearchReturnType, name: string, phrase: string): Promise<{ address: string, state: SignUpState } | undefined> => {
- const result = await search.getJsonUserByName(name);
-
- if (result?.address) {
- console.log("user %s already exists on the blockchain under the same name", name);
- return { address: result.bech32, state: SignUpState.user_already_exists_on_blockchain };
- }
-
- try {
- const address = await gnonative.addressFromMnemonic(phrase);
- const addressBech32 = await gnonative.addressToBech32(address);
- console.log("addressBech32", addressBech32);
-
- const accountNameStr = await gnonative.qEval("gno.land/r/demo/users", `GetUserByAddress("${addressBech32}").Name`);
- console.log("GetUserByAddress result:", accountNameStr);
- const accountName = accountNameStr.match(/\("(\w+)"/)?.[1];
- console.log("GetUserByAddress after regex", accountName);
-
- if (accountName) {
- console.log("user KEY already exists on the blockchain under name %s", accountName);
- return { address: addressBech32, state: SignUpState.user_already_exists_on_blockchain_under_different_name };
- }
-
- } catch (error) {
- console.error("error on qEval", error);
- return undefined;
- }
-
- return undefined;
-}
-
-const onboard = async (gnonative: GnoNativeApi, push: UseNotificationReturnType, account: KeyInfo | User) => {
- const { name, address } = account
- const address_bech32 = await gnonative.addressToBech32(address);
- console.log("onboarding %s, with address: %s", name, address_bech32);
-
- try {
- const hasBalance = await hasCoins(gnonative, address);
-
- if (hasBalance) {
- console.log("user %s already has a balance", name);
- await registerAccount(gnonative, name, account.address);
- return;
- }
-
- const response = await sendCoins(address_bech32);
- console.log("sent coins %s", response);
-
- await registerAccount(gnonative, name, account.address);
-
- push.registerDevice(address_bech32);
- } catch (error) {
- console.error("onboard error", error);
- }
-};
-
-const registerAccount = async (gnonative: GnoNativeApi, name: string, callerAddress: Uint8Array) => {
- console.log("Registering account %s", name);
- try {
- const gasFee = "10000000ugnot";
- const gasWanted = 20000000;
- const send = "200000000ugnot";
- const args: Array = ["", name, "Profile description"];
- for await (const response of await gnonative.call("gno.land/r/demo/users", "Register", args, gasFee, gasWanted, callerAddress, send)) {
- console.log("response: ", JSON.stringify(response));
- }
- } catch (error) {
- Alert.alert("Error on registering account", "" + error);
- console.error("error registering account", error);
- }
-};
-
-const hasCoins = async (gnonative: GnoNativeApi, address: Uint8Array) => {
- try {
- console.log("checking if user has balance");
- const balance = await gnonative.queryAccount(address);
- console.log("account balance: %s", balance.accountInfo?.coins);
-
- if (!balance.accountInfo) return false;
-
- const hasCoins = balance.accountInfo.coins.length > 0;
- const hasBalance = hasCoins && balance.accountInfo.coins[0].amount > 0;
-
- return hasBalance;
- } catch (error: any) {
- console.error("error on hasBalance", error["rawMessage"]);
- if (error["rawMessage"] === "invoke bridge method error: unknown: ErrUnknownAddress(#206)") return false;
- return false;
- }
-};
-
-const sendCoins = async (address: string) => {
- const myHeaders = new Headers();
- myHeaders.append("Content-Type", "application/json");
-
- const raw = JSON.stringify({
- To: address,
- });
-
- const requestOptions = {
- method: "POST",
- headers: myHeaders,
- body: raw,
- reactNative: { textStreaming: true },
- };
-
- // @ts-ignore
- const faucetRemote = process.env.EXPO_PUBLIC_FAUCET_REMOTE;
- if (!faucetRemote) {
- throw new Error("faucet remote address is undefined");
- }
-
- console.log("sending coins to %s on %s", address, faucetRemote);
-
- return fetch(faucetRemote, requestOptions);
-};
-
-export const signUpSlice = createSlice({
- name: "signUp",
- initialState,
- reducers: {
- signUpState: (state, action: PayloadAction) => {
- state.signUpState = action.payload
- },
- addProgress: (state, action: PayloadAction) => {
- console.log("progress--->", action.payload);
- state.progress = [...state.progress, '- ' + action.payload];
- },
- clearProgress: (state) => {
- state.progress = [];
- },
- clearSignUpState: (state) => {
- state.loading = false;
- state.newAccount = undefined;
- state.existingAccount = undefined;
- state.signUpState = undefined;
- }
- },
- extraReducers(builder) {
- builder.addCase(signUp.rejected, (state, action) => {
- action.error.message ? state.progress = [...state.progress, action.error.message] : null;
- console.error("signUp.rejected", action);
- }).addCase(signUp.fulfilled, (state, action) => {
- state.loading = false;
- state.newAccount = action.payload?.newAccount;
- state.existingAccount = action.payload?.existingAccount;
- state.signUpState = action.payload?.state;
- }).addCase(onboarding.fulfilled, (state, action) => {
- state.loading = false;
- state.newAccount = action.payload?.newAccount;
- state.existingAccount = action.payload?.existingAccount;
- state.signUpState = action.payload?.state;
- })
- },
-
- selectors: {
- selectLoading: (state) => state.loading,
- selectProgress: (state) => state.progress,
- signUpStateSelector: (state) => state.signUpState,
- newAccountSelector: (state) => state.newAccount,
- existingAccountSelector: (state) => state.existingAccount,
- },
-});
-
-export const { addProgress, signUpState, clearProgress, clearSignUpState } = signUpSlice.actions;
-
-export const { selectLoading, selectProgress, signUpStateSelector, newAccountSelector, existingAccountSelector } = signUpSlice.selectors;
diff --git a/mobile/redux/redux-provider.tsx b/mobile/redux/redux-provider.tsx
index 30a538cb..e284b35c 100644
--- a/mobile/redux/redux-provider.tsx
+++ b/mobile/redux/redux-provider.tsx
@@ -1,9 +1,8 @@
import React from "react";
import { Provider } from "react-redux";
import { configureStore } from "@reduxjs/toolkit";
-import { accountSlice, profileSlice, replySlice } from "./features";
+import { accountSlice, profileSlice, replySlice, linkingSlice } from "./features";
import { GnoNativeApi, useGnoNativeContext } from "@gnolang/gnonative";
-import { signUpSlice } from "./features/signupSlice";
import { useSearch, UseSearchReturnType } from "@gno/hooks/use-search";
import { useNotificationContext, UseNotificationReturnType } from "@gno/provider/notification-provider";
import { useUserCache } from "@gno/hooks/use-user-cache";
@@ -17,6 +16,15 @@ export interface ThunkExtra {
extra: { gnonative: GnoNativeApi; search: UseSearchReturnType; push: UseNotificationReturnType, userCache: ReturnType };
}
+const reducer = {
+ [accountSlice.reducerPath]: accountSlice.reducer,
+ [profileSlice.reducerPath]: profileSlice.reducer,
+ [replySlice.reducerPath]: replySlice.reducer,
+ [linkingSlice.reducerPath]: linkingSlice.reducer,
+}
+
+export type RootState = typeof reducer
+
const ReduxProvider: React.FC = ({ children }) => {
// Exposing GnoNative API to reduxjs/toolkit
const { gnonative } = useGnoNativeContext();
@@ -25,12 +33,7 @@ const ReduxProvider: React.FC = ({ children }) => {
const userCache= useUserCache();
const store = configureStore({
- reducer: {
- [accountSlice.reducerPath]: accountSlice.reducer,
- [profileSlice.reducerPath]: profileSlice.reducer,
- [replySlice.reducerPath]: replySlice.reducer,
- [signUpSlice.reducerPath]: signUpSlice.reducer,
- },
+ reducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: false,
diff --git a/mobile/src/hooks/use-feed.ts b/mobile/src/hooks/use-feed.ts
index 9284b286..3ca057fd 100644
--- a/mobile/src/hooks/use-feed.ts
+++ b/mobile/src/hooks/use-feed.ts
@@ -107,22 +107,5 @@ export const useFeed = () => {
return nHomePosts;
}
- async function onGnod(post: Post, callerAddress: Uint8Array) : Promise {
-
- try {
- const gasFee = "1000000ugnot";
- const gasWanted = 2000000;
- // post.user.address is in fact a bech32 address
- const args: Array = [String(post.user.address), String(post.id), String(post.id), String("0")];
- console.log("AddReaction args2: ", args.join(", "));
- for await (const response of await gnonative.call("gno.land/r/berty/social", "AddReaction", args, gasFee, gasWanted, callerAddress)) {
- const result = JSON.parse(JSON.stringify(response)).result;
- // Alert.alert("AddReaction Result", base64.decode(result));
- }
- } catch (error) {
- Alert.alert("Error", "Error while adding reaction: " + error);
- }
- }
-
- return { fetchFeed, fetchCount, fetchThread, fetchThreadPosts, onGnod };
+ return { fetchFeed, fetchCount, fetchThread, fetchThreadPosts };
};
diff --git a/mobile/src/hooks/use-search.ts b/mobile/src/hooks/use-search.ts
index e22e202d..bb1d2d4b 100644
--- a/mobile/src/hooks/use-search.ts
+++ b/mobile/src/hooks/use-search.ts
@@ -6,34 +6,6 @@ const MAX_RESULT = 10;
export const useSearch = () => {
const { gnonative } = useGnoNativeContext();
- async function Follow(address: string, callerAddress: Uint8Array) {
-
- try {
- const gasFee = "1000000ugnot";
- const gasWanted = 10000000;
- const args: Array = [address];
- for await (const response of await gnonative.call("gno.land/r/berty/social", "Follow", args, gasFee, gasWanted, callerAddress)) {
- console.log("response: ", JSON.stringify(response));
- }
- } catch (error) {
- console.error("error registering account", error);
- }
- }
-
- async function Unfollow(address: string, callerAddress: Uint8Array) {
-
- try {
- const gasFee = "1000000ugnot";
- const gasWanted = 10000000;
- const args: Array = [address];
- for await (const response of await gnonative.call("gno.land/r/berty/social", "Unfollow", args, gasFee, gasWanted, callerAddress)) {
- console.log("response: ", JSON.stringify(response));
- }
- } catch (error) {
- console.error("error registering account", error);
- }
- }
-
async function GetJsonFollowersCount(address: string) {
const { n_followers } = await GetJsonFollowers(address);
@@ -103,8 +75,6 @@ export const useSearch = () => {
GetJsonFollowersCount,
GetJsonFollowing,
GetJsonFollowers,
- Follow,
- Unfollow,
};
};
diff --git a/mobile/src/provider/linking-provider.tsx b/mobile/src/provider/linking-provider.tsx
new file mode 100644
index 00000000..af3babd0
--- /dev/null
+++ b/mobile/src/provider/linking-provider.tsx
@@ -0,0 +1,25 @@
+import * as Linking from 'expo-linking';
+import { useEffect } from 'react';
+import { setLinkingData, useAppDispatch } from "@gno/redux";
+
+
+const LinkingProvider = ({ children }: { children: React.ReactNode }) => {
+ const url = Linking.useURL();
+
+ const dispatch = useAppDispatch();
+
+ useEffect(() => {
+ (async () => {
+ if (url) {
+ const linkingParsedURL = Linking.parse(url);
+ console.log("link url received", url);
+
+ await dispatch(setLinkingData(linkingParsedURL));
+ }
+ })();
+ }, [url]);
+
+ return <>{children}>;
+};
+
+export { LinkingProvider };
\ No newline at end of file