Skip to content

Commit

Permalink
feat: login at gnokey
Browse files Browse the repository at this point in the history
  • Loading branch information
iuricmp committed Oct 8, 2024
1 parent 71b373d commit 41c38e8
Show file tree
Hide file tree
Showing 5 changed files with 96 additions and 103 deletions.
99 changes: 21 additions & 78 deletions mobile/app/index.tsx
Original file line number Diff line number Diff line change
@@ -1,83 +1,36 @@
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, selectPath, selectQueryParamsAddress, 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<KeyInfo[]>([]);
const [loading, setLoading] = useState<string | undefined>(undefined);
const [reenterPassword, setReenterPassword] = useState<KeyInfo | undefined>(undefined);

const { gnonative } = useGnoNativeContext();
const navigation = useNavigation();
const dispatch = useAppDispatch();
const route = useRouter();
const path = useAppSelector(selectPath)
const bech32 = useAppSelector(selectQueryParamsAddress)

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 }));
useEffect(() => {
if (path === "login-callback" && bech32) {
dispatch(loggedIn({ bech32: bech32 as string }));
dispatch(clearLinking());
setTimeout(() => route.replace("/home"), 500);
} catch (error: unknown | Error) {
setLoading(error?.toString());
console.log(error);
}
};
}, [path, bech32]);

const onCloseReenterPassword = async (sucess: boolean) => {
if (sucess && reenterPassword) {
await dispatch(loggedIn({ keyInfo: reenterPassword }));
setTimeout(() => route.replace("/home"), 500);
}
setReenterPassword(undefined);
};

if (loading) {
return (
<Layout.Container>
<Layout.Body>
<Text.Title>{loading}</Text.Title>
</Layout.Body>
</Layout.Container>
);
}

const signinUsingGnokey = async () => {
await dispatch(requestLoginForGnokeyMobile()).unwrap();
};

return (
<>
Expand All @@ -90,25 +43,15 @@ export default function Root() {
<Text.Caption1>v{appVersion}</Text.Caption1>
</View>

<ScrollView style={{ marginTop: 24 }}>
{accounts && accounts.length > 0 && (
<>
<Text.Caption1>Please, select one of the existing accounts to start:</Text.Caption1>
<SideMenuAccountList accounts={accounts} changeAccount={onChangeAccountHandler} />
<Spacer />
</>
)}
<Spacer />
</ScrollView>
<View style={{ flex: 1 }}>
{/* Hero copy */}
</View>
<Ruller />
<Spacer />
<Text.Caption1>Or create a new account:</Text.Caption1>
<Button.Link title="Sign up" href="sign-up" />
<Text.Caption1>Sign in using Gnokey Mobile:</Text.Caption1>
<Button.TouchableOpacity title="Sign in" onPress={signinUsingGnokey} variant="primary" />
</Layout.BodyAlignedBotton>
</Layout.Container>
{reenterPassword ? (
<ReenterPassword visible={Boolean(reenterPassword)} accountName={reenterPassword.name} accountAddress={reenterPassword.address} onClose={onCloseReenterPassword} />
) : null}
</>
);
}
5 changes: 4 additions & 1 deletion mobile/components/auth/guard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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("/");
Expand Down
24 changes: 18 additions & 6 deletions mobile/redux/features/accountSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,33 @@ const initialState: CounterState = {
};

interface LoginParam {
keyInfo: KeyInfo;
bech32: string;
}

export const loggedIn = createAsyncThunk<User, LoginParam, ThunkExtra>("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;
});

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
}

export const saveAvatar = createAsyncThunk<void, { mimeType: string, base64: string }, ThunkExtra>("account/saveAvatar", async (param, thunkAPI) => {
const { mimeType, base64 } = param;

Expand Down Expand Up @@ -92,6 +103,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);
Expand Down
47 changes: 40 additions & 7 deletions mobile/redux/features/linkingSlice.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,32 @@
import { MakeTxResponse } from "@gnolang/gnonative";
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
import { set } from "date-fns";
import * as Linking from 'expo-linking';
import { RootState, ThunkExtra } from "redux/redux-provider";
import { ThunkExtra } from "redux/redux-provider";

interface State {
linkingParsedURl: Linking.ParsedURL | undefined;
queryParams: Linking.QueryParams | undefined;
path: string | undefined;
hostname: string | undefined;
}

const initialState: State = {
linkingParsedURl: undefined,
queryParams: undefined,
path: undefined,
hostname: undefined,
};

export const hasParam = (param: string, queryParams: Linking.QueryParams | undefined): boolean => {
return Boolean(queryParams && queryParams[param] !== undefined);
}

export const requestLoginForGnokeyMobile = createAsyncThunk<boolean>("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}`);
})

export const requestAddressForGnokeyMobile = createAsyncThunk<boolean>("tx/requestAddressForGnokeyMobile", async () => {
console.log("requesting address for GnokeyMobile");
const callback = encodeURIComponent('tech.berty.dsocial://post');
Expand Down Expand Up @@ -48,25 +57,49 @@ export const broadcastTxCommit = createAsyncThunk<void, string, ThunkExtra>("tx/
await gnonative.broadcastTxCommit(signedTx);
});

type SetLinkingResponse = Partial<State>;

export const setLinkingParsedURL = createAsyncThunk<SetLinkingResponse, Linking.ParsedURL, ThunkExtra>("tx/setLinkingParsedURL", async (linkingParsedURl, thunkAPI) => {
const { hostname, path, queryParams } = linkingParsedURl;

return {
linkingParsedURl,
queryParams: queryParams || undefined,
path: path || undefined,
hostname: hostname || undefined,
}
})

/**
* Slice to handle linking between the app and the GnokeyMobile app
*/
export const linkingSlice = createSlice({
name: "linking",
initialState,
extraReducers: (builder) => {
builder.addCase(setLinkingParsedURL.fulfilled, (state, action) => {
state.linkingParsedURl = action.payload.linkingParsedURl;
state.queryParams = action.payload.queryParams;
state.path = action.payload.path;
state.hostname = action.payload.hostname;
})
},
reducers: {
setLinkingParsedURL: (state, action) => {
state.linkingParsedURl = action.payload;
state.queryParams = action.payload?.queryParams;
clearLinking: (state) => {
state.linkingParsedURl = undefined;
state.queryParams = undefined;
state.path = undefined;
state.hostname = undefined;
}
},
selectors: {
selectPath: (state: State) => state.path,
selectQueryParams: (state: State) => state.queryParams,
selectLinkingParsedURL: (state: State) => state.linkingParsedURl,
selectQueryParamsAddress: (state: State) => state.linkingParsedURl?.queryParams?.address,
},
});

export const { setLinkingParsedURL } = linkingSlice.actions;
export const { clearLinking } = linkingSlice.actions;

export const { selectLinkingParsedURL, selectQueryParams, selectQueryParamsAddress } = linkingSlice.selectors;
export const { selectLinkingParsedURL, selectQueryParams, selectQueryParamsAddress, selectPath } = linkingSlice.selectors;
24 changes: 13 additions & 11 deletions mobile/src/provider/linking-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,19 @@ const LinkingProvider = ({ children }: { children: React.ReactNode }) => {
const dispatch = useAppDispatch();

useEffect(() => {
if (url) {
const linkingParsedURL = Linking.parse(url);
const { hostname, path, queryParams } = linkingParsedURL;

console.log("link url", url);
console.log("link hostname", hostname);
console.log("link path", path);
console.log("link queryParams", queryParams);

dispatch(setLinkingParsedURL(linkingParsedURL))
}
(async () => {
if (url) {
const linkingParsedURL = Linking.parse(url);
const { hostname, path, queryParams } = linkingParsedURL;

console.log("link url", url);
console.log("link hostname", hostname);
console.log("link path", path);
console.log("link queryParams", queryParams);

await dispatch(setLinkingParsedURL(linkingParsedURL)).unwrap();
}
})();
}, [url]);

return <>{children}</>;
Expand Down

0 comments on commit 41c38e8

Please sign in to comment.