Skip to content

Commit

Permalink
Merge pull request #2754 from getAlby/lnurl-auth-onboarding
Browse files Browse the repository at this point in the history
feat: lnurl-auth onboarding
  • Loading branch information
rolznz authored Oct 5, 2023
2 parents e4e03ef + be82d54 commit 20d5b7d
Show file tree
Hide file tree
Showing 7 changed files with 188 additions and 161 deletions.
140 changes: 140 additions & 0 deletions src/app/components/LNURLAuth/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import Button from "@components/Button";
import ConfirmOrCancel from "@components/ConfirmOrCancel";
import Container from "@components/Container";
import ContentMessage from "@components/ContentMessage";
import PublisherCard from "@components/PublisherCard";
import ResultCard from "@components/ResultCard";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import ScreenHeader from "~/app/components/ScreenHeader";
import { useNavigationState } from "~/app/hooks/useNavigationState";
import { USER_REJECTED_ERROR } from "~/common/constants";
import api from "~/common/lib/api";
import msg from "~/common/lib/msg";
import type { LNURLAuthServiceResponse } from "~/types";

function LNURLAuthComponent() {
const { t } = useTranslation("translation", { keyPrefix: "lnurlauth" });
const { t: tCommon } = useTranslation("common");

const navigate = useNavigate();
const navState = useNavigationState();

const details = navState.args?.lnurlDetails as LNURLAuthServiceResponse;
const origin = navState.origin;

const [successMessage, setSuccessMessage] = useState("");
const [errorMessage, setErrorMessage] = useState("");
const [loading, setLoading] = useState(false);

async function confirm() {
try {
setLoading(true);
const response = await api.lnurlAuth({
origin,
lnurlDetails: details,
});

if (navState.isPrompt && origin?.host) {
const allowance = await api.getAllowance(origin.host);

if (allowance.lnurlAuth === false) {
await msg.request("updateAllowance", {
id: allowance.id,
lnurlAuth: true,
});
}
}

if (response.success) {
setSuccessMessage(
t("success", { name: origin ? origin.name : details.domain })
);
// ATTENTION: if this LNURL is called through `webln.lnurl` then we immediately return and return the response. This closes the window which means the user will NOT see the above successAction.
// We assume this is OK when it is called through webln.
if (navState.isPrompt) {
msg.reply(response);
}
} else {
setErrorMessage(t("errors.status"));
}
} catch (e) {
console.error(e);
if (e instanceof Error) {
setErrorMessage(`Error: ${e.message}`);
}
} finally {
setLoading(false);
}
}

function reject(e: React.MouseEvent<HTMLButtonElement>) {
e.preventDefault();
if (navState.isPrompt) {
msg.error(USER_REJECTED_ERROR);
} else {
navigate(-1);
}
}

function close(e: React.MouseEvent<HTMLButtonElement>) {
// will never be reached via prompt
e.preventDefault();
navigate(-1);
}

return (
<div className="h-full flex flex-col overflow-y-auto no-scrollbar">
<ScreenHeader title={t("title")} />
{!successMessage ? (
<>
<Container justifyBetween maxWidth="sm">
<div>
{origin ? (
<PublisherCard
title={origin.name}
image={origin.icon}
url={details.domain}
/>
) : (
<PublisherCard title={details.domain} />
)}

<ContentMessage
heading={`${t("content_message.heading")} ${
origin ? origin.name : details.domain
}?`}
content={details.domain}
/>

{errorMessage && (
<p className="my-2 mx-5 text-red-500">{errorMessage}</p>
)}
</div>
<ConfirmOrCancel
label={t("submit")}
onConfirm={confirm}
onCancel={reject}
disabled={loading}
loading={loading}
/>
</Container>
</>
) : (
<Container justifyBetween maxWidth="sm">
<ResultCard isSuccess message={successMessage} />
<div className="mt-4">
<Button
onClick={close}
label={tCommon("actions.close")}
fullWidth
/>
</div>
</Container>
)}
</div>
);
}

export default LNURLAuthComponent;
158 changes: 28 additions & 130 deletions src/app/screens/LNURLAuth/index.tsx
Original file line number Diff line number Diff line change
@@ -1,140 +1,38 @@
import Button from "@components/Button";
import ConfirmOrCancel from "@components/ConfirmOrCancel";
import Container from "@components/Container";
import ContentMessage from "@components/ContentMessage";
import PublisherCard from "@components/PublisherCard";
import ResultCard from "@components/ResultCard";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import ScreenHeader from "~/app/components/ScreenHeader";
import { useNavigationState } from "~/app/hooks/useNavigationState";
import { USER_REJECTED_ERROR } from "~/common/constants";
import { useEffect, useState } from "react";
import LNURLAuthComponent from "~/app/components/LNURLAuth";
import Onboard from "~/app/components/onboard";
import { useAccount } from "~/app/context/AccountContext";
import { isAlbyOAuthAccount } from "~/app/utils";
import api from "~/common/lib/api";
import msg from "~/common/lib/msg";
import type { LNURLAuthServiceResponse } from "~/types";

function LNURLAuth() {
const { t } = useTranslation("translation", { keyPrefix: "lnurlauth" });
const { t: tCommon } = useTranslation("common");

const navigate = useNavigate();
const navState = useNavigationState();

const details = navState.args?.lnurlDetails as LNURLAuthServiceResponse;
const origin = navState.origin;

const [successMessage, setSuccessMessage] = useState("");
const [errorMessage, setErrorMessage] = useState("");
const [loading, setLoading] = useState(false);

async function confirm() {
try {
setLoading(true);
const response = await api.lnurlAuth({
origin,
lnurlDetails: details,
});

if (navState.isPrompt && origin?.host) {
const allowance = await api.getAllowance(origin.host);

if (allowance.lnurlAuth === false) {
await msg.request("updateAllowance", {
id: allowance.id,
lnurlAuth: true,
});
export default function LNURLAuth() {
const { account } = useAccount();
const [hasMnemonic, setHasMnemonic] = useState(false);
const [albyOAuthAccount, setAlbyOAuthAccount] = useState(false);

useEffect(() => {
async function fetchAccountInfo() {
try {
const fetchedAccount = await api.getAccount();
const isOAuthAccount = isAlbyOAuthAccount(fetchedAccount.connectorType);
setAlbyOAuthAccount(isOAuthAccount);

if (fetchedAccount.hasMnemonic) {
setHasMnemonic(true);
} else {
setHasMnemonic(false);
}
} catch (e) {
console.error(e);
}

if (response.success) {
setSuccessMessage(
t("success", { name: origin ? origin.name : details.domain })
);
// ATTENTION: if this LNURL is called through `webln.lnurl` then we immediately return and return the response. This closes the window which means the user will NOT see the above successAction.
// We assume this is OK when it is called through webln.
if (navState.isPrompt) {
msg.reply(response);
}
} else {
setErrorMessage(t("errors.status"));
}
} catch (e) {
console.error(e);
if (e instanceof Error) {
setErrorMessage(`Error: ${e.message}`);
}
} finally {
setLoading(false);
}
}

function reject(e: React.MouseEvent<HTMLButtonElement>) {
e.preventDefault();
if (navState.isPrompt) {
msg.error(USER_REJECTED_ERROR);
} else {
navigate(-1);
}
}

function close(e: React.MouseEvent<HTMLButtonElement>) {
// will never be reached via prompt
e.preventDefault();
navigate(-1);
}
fetchAccountInfo();
}, [account]);

return (
<div className="h-full flex flex-col overflow-y-auto no-scrollbar">
<ScreenHeader title={t("title")} />
{!successMessage ? (
<>
<Container justifyBetween maxWidth="sm">
<div>
{origin ? (
<PublisherCard
title={origin.name}
image={origin.icon}
url={details.domain}
/>
) : (
<PublisherCard title={details.domain} />
)}

<ContentMessage
heading={`${t("content_message.heading")} ${
origin ? origin.name : details.domain
}?`}
content={details.domain}
/>

{errorMessage && (
<p className="my-2 mx-5 text-red-500">{errorMessage}</p>
)}
</div>
<ConfirmOrCancel
label={t("submit")}
onConfirm={confirm}
onCancel={reject}
disabled={loading}
loading={loading}
/>
</Container>
</>
) : (
<Container justifyBetween maxWidth="sm">
<ResultCard isSuccess message={successMessage} />
<div className="mt-4">
<Button
onClick={close}
label={tCommon("actions.close")}
fullWidth
/>
</div>
</Container>
)}
</div>
<>
{albyOAuthAccount && !hasMnemonic ? <Onboard /> : <LNURLAuthComponent />}
</>
);
}

export default LNURLAuth;
25 changes: 19 additions & 6 deletions src/extension/background-script/actions/lnurl/authOrPrompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,9 @@ async function authOrPrompt(
// we have the check the unlock status manually. The account can still be locked
// If it is locked we must show a prompt to unlock
const isUnlocked = await state.getState().isUnlocked();
const account = await state.getState().getAccount();

// check if there is a publisher and lnurlAuth is enabled,
// otherwise we we prompt the user
if (isUnlocked && allowance && allowance.enabled && allowance.lnurlAuth) {
return await authFunction({ lnurlDetails, origin: message.origin });
} else {
async function authPrompt() {
try {
const promptMessage = {
...message,
Expand All @@ -49,12 +46,28 @@ async function authOrPrompt(
},
};

return await utils.openPrompt<LnurlAuthResponse>(promptMessage);
const response = await utils.openPrompt<LnurlAuthResponse>(promptMessage);
return response;
} catch (e) {
// user rejected
return { error: e instanceof Error ? e.message : e };
}
}

// check if there is a publisher and lnurlAuth is enabled,
// otherwise we we prompt the user

if (
isUnlocked &&
allowance &&
allowance.enabled &&
allowance.lnurlAuth &&
(!account?.useMnemonicForLnurlAuth || account?.mnemonic)
) {
return await authFunction({ lnurlDetails, origin: message.origin });
}

return await authPrompt();
}

export default authOrPrompt;
2 changes: 1 addition & 1 deletion src/extension/background-script/actions/lnurl/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,4 @@ async function lnurl(message: MessageWebLnLnurl, sender: Sender) {
}

export default lnurl;
export { authOrPrompt, payWithPrompt, withdrawWithPrompt, auth };
export { auth, authOrPrompt, payWithPrompt, withdrawWithPrompt };
3 changes: 0 additions & 3 deletions src/extension/background-script/actions/onboard/index.ts

This file was deleted.

19 changes: 0 additions & 19 deletions src/extension/background-script/actions/onboard/prompt.ts

This file was deleted.

Loading

0 comments on commit 20d5b7d

Please sign in to comment.