Open KartikBazzad opened 1 year ago
I would like to work on this issue please
This is where the error occurs:
Looks like href is read only in react native
I try but don't show login form of google. Please check. Thanks
React-native can't just open a web url. The Appwrite SDK was designed for web browsers, which make redirections, and for that reason certain parts like OAuth authentication won't work out of the box in react-native. There are a few ways around this though:
Hope this helps
Note: this might be relevant for issue #1177797467
I was looking for a solution to avoid closed common other backend services. I think AppWrite is doing that well and it would be so useful if AppWrite supports oauth with reactive-native in the future.
We're working a new dedicated React Native SDK. Moving this issue to the new repository
Hey, nice that you're putting some efforts on a dedicated react-native sdk! :)
Turns out that I've been trying appwrite in a RN app and this was the first thing that I came across.
I managed to workaround the issue for now by overriding the Account
and doing my own implementation based on the other appwrite mobile sdks.
I've used expo-web-browser
for the auth session (react-native-inappbrowser-reborn
is another option), along with @react-native-cookies/cookies
to set the needed cookie.
If anybody's interested on it until it's implemented in the sdk, here are my snippets:
import * as WebBrowser from "expo-web-browser";
import { Account, AppwriteException, OAuthProvider } from "appwrite";
import { handleIncomingCookie } from "./handleIncomingCookie";
import { buildQueryParams } from "./uri";
export class OauthAwareAccount extends Account {
/**
* @deprecated Temporaryly use {@link createOAuthSession} instead, can't use this because of its return type (not a promise)
* @param provider
* @param success
* @param failure
* @param scopes
* @returns
*/
override createOAuth2Session(
provider: OAuthProvider,
success?: string | undefined,
failure?: string | undefined,
scopes?: string[] | undefined,
): void | URL {
return super.createOAuth2Session(provider, success, failure, scopes);
}
async createOAuthSession(
provider: OAuthProvider,
success?: string | undefined,
failure?: string | undefined,
scopes?: string[] | undefined,
): Promise<void | URL> {
if (!provider) {
throw new AppwriteException('Missing required parameter: "provider"');
}
const { endpoint, project } = this.client.config;
const apiPath = `/account/sessions/oauth2/${provider}`;
const payload: Record<string, string | string[] | undefined> = {
success,
failure,
scopes,
project,
};
const queryParams = buildQueryParams(payload);
const authUrl = `${endpoint}${apiPath}${queryParams ? `?${queryParams}` : ""}`;
const callbackUrl = `appwrite-callback-${project}`;
const browserResult = await WebBrowser.openAuthSessionAsync(
authUrl,
callbackUrl,
);
if (browserResult.type !== "success") {
return;
}
const url = browserResult.url;
if (!(await handleIncomingCookie(url, endpoint))) {
return;
}
return new URL(url);
}
}
import CookieManager, { Cookie } from "@react-native-cookies/cookies";
import { AppwriteException } from "appwrite";
import { parseQueryParams } from "./uri";
export const handleIncomingCookie = async (url: string, endpoint: string) => {
if (!url.includes("appwrite-callback")) {
return false;
}
const queryParams = parseQueryParams(url);
if (!queryParams.key || !queryParams.secret || !queryParams.domain) {
throw new AppwriteException(
"Invalid OAuth2 Response. Key, Secret and Domain not available.",
500,
);
}
const domainUrl = new URL(endpoint);
const cookie: Cookie = {
name: queryParams.key,
value: queryParams.secret,
path: queryParams.path,
expires: queryParams.expires,
secure: "secure" in queryParams,
httpOnly: "httpOnly" in queryParams,
domain: domainUrl.hostname,
};
return CookieManager.set(domainUrl.toString(), cookie);
};
The sdk has some utilities for this already, like flatten
in service.ts
, I didn't use that because I based my implementation on Flutter's for this. I wasn't sure if I should encodeURIComponent
or not, for example. Seems to work like this. Additionally, I didn't implement the recursive flatten
, I do that with a simple map
, which should be fine too.
export const buildQueryParams = (
params: Record<string, string | string[] | undefined>,
) =>
Object.keys(params).reduce((acc, currentKey) => {
const currentValueForKey = params[currentKey];
if (currentValueForKey === undefined) {
return acc;
}
if (Array.isArray(currentValueForKey)) {
const arrayQuery = currentValueForKey
.map(
(value) =>
`${encodeURIComponent(`${currentKey}[]`)}=${encodeURIComponent(value)}`,
)
.join("&");
return `${acc}&${arrayQuery}`;
}
return `${acc}&${encodeURIComponent(currentKey)}=${encodeURIComponent(currentValueForKey)}`;
}, "");
export const parseQueryParams = (url: string) => {
const queryParams = url.includes("?") ? url.split("?")[1] : url;
if (!queryParams) {
return {};
}
return queryParams.split("&").reduce(
(acc, curr) => {
const [key, value] = curr.split("=");
return { ...acc, [key as string]: value };
},
{} as Record<string, string | undefined>,
);
};
To use the code above simply instantiate a new OauthAwareAccount(client)
rather than the Account
from the sdk in your appwrite client configuration.
@eldadfux depending on what you envision for this on the react native sdk, I can PR something, just let me know.
Hope this helps in some way!
A simpler way to do it is with the following code:
const url = account.createOAuth2Token(OAuthProvider.Google, 'here-call-back-url');
const browserResult = await WebBrowser.openAuthSessionAsync(url.href, 'here-call-back-url');
const urlObject = new URL(browserResult.url);
const secret = urlObject.searchParams.get('secret');
const userId = urlObject.searchParams.get('userId');
const session = await appwrite.account.createSession(userId, secret);
In summary, it is using createOAuth2Token instead of createOAuth2Session because createOAuth2Token returns secret and userId which are necessary to create the session.
A simpler way to do it is with the following code:
const url = account.createOAuth2Token(OAuthProvider.Google, 'here-call-back-url'); const browserResult = await WebBrowser.openAuthSessionAsync(url.href, 'here-call-back-url'); const urlObject = new URL(browserResult.url); const secret = urlObject.searchParams.get('secret'); const userId = urlObject.searchParams.get('userId'); const session = await appwrite.account.createSession(userId, secret);
In summary, it is using createOAuth2Token instead of createOAuth2Session because createOAuth2Token returns secret and userId which are necessary to create the session.
could you tell me what is 'here-call-back-url' i don't get it
could you tell me what is 'here-call-back-url' i don't get it
The url to deep link back into your app.
A simpler way to do it is with the following code:
const url = account.createOAuth2Token(OAuthProvider.Google, 'here-call-back-url'); const browserResult = await WebBrowser.openAuthSessionAsync(url.href, 'here-call-back-url'); const urlObject = new URL(browserResult.url); const secret = urlObject.searchParams.get('secret'); const userId = urlObject.searchParams.get('userId'); const session = await appwrite.account.createSession(userId, secret);
In summary, it is using createOAuth2Token instead of createOAuth2Session because createOAuth2Token returns secret and userId which are necessary to create the session.
could you tell me what is 'here-call-back-url' i don't get it
Thank you, that is what i need !
Without deeplink, you can use following logic.
import {
openAuthSessionAsync,
WebBrowserAuthSessionResult,
} from "expo-web-browser";
...
const [browserResult, setBrowserResult] =
useState<WebBrowserAuthSessionResult | null>(null);
...
const signInWithProvider = async (provider: OAuthProvider) => {
console.log(`Signing in with ${provider}`);
const url = createOAuth2Token(provider);
if (url) {
const urlString = url.toString();
console.log(urlString);
if (Platform.OS !== "web") {
// Prevent the default behavior of linking to the default browser on native.
// event.preventDefault();
const result = await openAuthSessionAsync(urlString);
setBrowserResult(result);
}
} else {
console.error("Failed to obtain URL for provider:", provider);
}
};
// authService.js
export function createOAuth2Token(provider: OAuthProvider) {
try {
const token = account.createOAuth2Token(
provider,
SUCCESS_OAUTH2,
FAILURE_OAUTH2
);
return token;
} catch (error) {
throw new Error(error);
}
}
export async function createSession(userId: string, secret: string) {
try {
const session = await account.createSession(userId, secret);
return session;
} catch (error) {
throw new Error(error);
}
}
π Reproduction steps
Start a new React Native project. Enable OAuth.
π Expected behavior
Should have opened a window for selecting an account from the OAuth client like google
π Actual Behavior
Throwed Error
π² Appwrite version
Different version (specify in environment)
π» Operating system
Windows
𧱠Your Environment
I am using Appwrite v:1.0.1.500 React Native 0.70.1 React 18.1.0
π Have you spent some time to check if this issue has been raised before?
π’ Have you read the Code of Conduct?