Hello, I am trying to connect users to my web app completely hosted on the IC with II which is working fine on chrome but safari is blocking the pop-up, I guess because client.login is an async call so I’m calling it inside an async function which safari doesn’t allows (?). but I’ve checked some other apps such as http://nns.ic0.app and open chat which seems to work fine (even though they are also opening in a different tab) any clue on how I could go about fixing this?.
Any help is appreciated.
P.S I also have NFID integrated and it seems to be working just fine
If I’m not wrong, Safari doesn’t have any issues with opening popups using async code. However, it does require user interaction to do so. Is your login triggered by a button with a click
event?
yes, though the button is inside of a modal.
I would like to mention that auth.login isn’t called directly inside the click
event (it is nested inside).
this is the code snippet:
const methods = authMethods({
useAuthStore,
setIsLoading,
handleClose,
});
async function handleLogin(type: LoginEnum) {
setSelected(type);
methods.login(type, navigation, callBackfn);
}
The login function:
const login = async (
type: LoginEnum,
navigation: any,
callBackfn: () => void,
) => {
let ran = false;
if (
auth &&
auth.state === 'anonymous' &&
handleClose &&
setIsLoading
) {
setIsLoading(true);
if (type == LoginEnum.InternetIdentity) {
const client = await AuthClient.create({
idleOptions: {
idleTimeout: (1000 * 60 * 60) / 2,
},
});
await client?.login({
maxTimeToLive: BigInt(1800) * BigInt(1_000_000_000),
// windowOpenerFeatures:
// 'toolbar=0,location=0,menubar=0,width=500,height=500,left=100,top=100',
identityProvider:
process.env.NEXT_PUBLIC_DFX_NETWORK === 'ic'
? 'https://identity.ic0.app/#authorize'
: `http://${process.env.CANISTER_ID_INTERNET_IDENTITY}.localhost:4943/#authorize`,
onSuccess: () => {
authenticate(client as AuthClient).then(() => {
const path = window?.location;
if (path?.pathname == '/') navigation.push(DASHBOARD_ROUTE);
if (callBackfn) {
callBackfn();
}
});
},
onError: () => {
handleClose();
},
});
} else if (type == LoginEnum.NFID) {
setIsLoading(true);
try {
const nfid = await NFID.init({
application: {
name: appData.name,
logo: appData.logo,
},
idleOptions: {
idleTimeout: 45 * 60 * 1000,
captureScroll: true,
},
});
const delegationIdentity: Identity = await nfid.getDelegation({
maxTimeToLive: BigInt(2) * BigInt(3_600_000_000_000),
});
authenticate(undefined, delegationIdentity).then(() => {
const path = window?.location;
if (path?.pathname == '/') path.assign(DASHBOARD_ROUTE);
});
} catch (error) {
setIsLoading(false);
}
} else if (auth && !ran && auth.state === 'anonymous') {
initAuth();
ran = true;
}
}
};
Does your button calls handleLogin
?
If for testing purpose you replace this code by a call to the login (await AuthClient.create(...)).login()
, does the popup open?
A bit hard to tell based on the snippet. Can you share a link to the repo?
yes it calls handleLogin
.
I’ll try
I’m sorry but this code isn’t public (yet) so I can’t really share the repo right now but if you want I can explain it in more details
Let me know if the test work. If it doesn’t, maybe you can try to share a minimal sample repo to reproduce the issue?
I’ve created the repo for the II authentication here.
And I did verify the issue it still persists (on Mac safari). I tested it by deploying to the playground
Thanks for the sample repo . I can reproduce the issue with it. After some debugging, I believe the problem is related to creating the
AuthClient
within the same function or click event that handles the login. If I instantiate the AuthClient
on mount and perform only the login within the function, then the popup opens correctly in Safari.
function HomePage() {
const [authClient, setAuthClient] = useState(undefined);
useEffect(() => {
(async () => {
const client = await AuthClient.create({
idleOptions: {
idleTimeout: (1000 * 60 * 60) / 2 // set to half an hour
}
})
setAuthClient(client);
})();
}, [])
async function handleLogin() {
// await AuthClient.create(...) <---- Doing this here too blocks the popup
await authClient?.login();
}
return (
<div className={styles.container}>
<Head>
<title>Internet Computer</title>
</Head>
<main className={styles.main}>
<h3 className={styles.title}>
Welcome to Next.js Internet Computer Starter Template!
</h3>
<button onClick={handleLogin}>Login</button>
</main>
</div>
)
}
export default HomePage
Thank you for helping debug, @peterparker!
@Mysticx his recommendation matches what I also recommend, as the maintainer of the library.
Safari can be very particular about how it allows new windows to be opened, so as a rule you should avoid any async behavior in between the click
handler and the function that opens the window.
AuthClient.create
is async because it needs to make an asynchronous query against indexeddb to see if you have a previously created delegation or key it can use to restore your session. That check is getting in between the click and opening the window.
The correct way to handle this is to construct the auth client during the mount
, init
, or equivalent phase of your app, and then to invoke the login
method directly from your click handler. Passing both onSuccess
and onError
handlers is also a best practice!
Thanks for the help! @peterparker,
I had the thought that I should try to move the client.create (so ashamed of myself rn).
it is still weird to me how safari doesn’t allow async calls in between the event and the pop-up opening function.
Yea this is what I’m doing in my actual implementation. (just couldn’t be bothered to add all that in the repo)
You saved the Fortune Wheel for multiple live conferences! Now the admin login button is working again!
cc @Severin
@kpeacock
I have the login code and it works on desktop, but on mobile it doesn’t open the window, whether new or tab, to log in.
I tried using the windowOpenerFeatures property, but it only works on desktop.
Is there any way to make it work on mobile? I tested it with browsers like brave, edge, safari and they all look the same, they don’t open any window or tab.
export async function IdentityAuthenticate(
dispatch: AppDispatch,
): Promise<void> {
try {
const authClient = await AuthClient.create()
const HTTP_AGENT_HOST = `${process.env.HTTP_AGENT_HOST}`
const AUTH_EXPIRATION_INTERNET_IDENTITY = BigInt(
30 * 24 * 60 * 60 * 1000 * 1000 * 1000,
)
let windowFeatures = undefined
const isDesktop = window.innerWidth > 768
if (isDesktop) {
const width = 500
const height = 600
const left = window.screenX + (window.innerWidth - width) / 2
const top = window.screenY + (window.innerHeight - height) / 2
windowFeatures = `left=${left},top=${top},width=${width},height=${height}`
}
await authClient.login({
maxTimeToLive: AUTH_EXPIRATION_INTERNET_IDENTITY,
identityProvider: HTTP_AGENT_HOST,
derivationOrigin: `${process.env.ENV_AUTH_DERIVATION_ORIGIN}`,
windowOpenerFeatures: windowFeatures,
onSuccess: () => {
const identity = authClient.getIdentity()
const myAgent = getAgent(identity)
doLogin(myAgent, dispatch)
},
onError: (error) => {
console.error('Internet Identity authentication failed', error)
},
})
} catch (error) {
console.error('Unexpected error during authentication process', error)
}
}
@msalvatti see my earlier comment in this thread - the best practice is to avoid async behavior between the click
event and the login
method.
Create your authClient
instance beforehand and just call login
when the user interacts. This is a browser behavior that we have no way of working around
@kpeacock I saw and followed your comment above, but the behavior remained the same, on mobile the new tab still does not open doing as you said or as I sent.
Would there be something different to do?
Try a pattern that looks more like this: auth-client-demo/src/auth_client_demo_assets/react/use-auth-client.jsx at main · krpeacock/auth-client-demo · GitHub
I have examples for React, Vue, Svelte, and vanilla JS. There’s also a hook library if you’re using React - @dfinity/use-auth-client - npm