From 924bc600786408709883d9df6a184dd83128a7b1 Mon Sep 17 00:00:00 2001 From: KONFeature Date: Thu, 2 May 2024 18:27:22 +0200 Subject: [PATCH 01/17] =?UTF-8?q?=E2=9C=A8=20Add=20simple=20referral=20act?= =?UTF-8?q?ions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../context/common/blockchain/addresses.ts | 2 + .../src/context/referral/abi/campaign-abis.ts | 826 ++++++++++++++++++ .../context/referral/action/userReferred.ts | 51 ++ .../referral/action/userReferredOnContent.ts | 58 ++ 4 files changed, 937 insertions(+) create mode 100644 packages/wallet/src/context/referral/abi/campaign-abis.ts create mode 100644 packages/wallet/src/context/referral/action/userReferred.ts create mode 100644 packages/wallet/src/context/referral/action/userReferredOnContent.ts diff --git a/packages/wallet/src/context/common/blockchain/addresses.ts b/packages/wallet/src/context/common/blockchain/addresses.ts index 0890bae3d..ee484096b 100644 --- a/packages/wallet/src/context/common/blockchain/addresses.ts +++ b/packages/wallet/src/context/common/blockchain/addresses.ts @@ -6,6 +6,8 @@ export const addresses = { paywallToken: "0x9584A61F70cC4BEF5b8B5f588A1d35740f0C7ae2", paywall: "0x9218521020EF26924B77188f4ddE0d0f7C405f21", communityToken: "0xf98BA1b2fc7C55A01Efa6C8872Bcee85c6eC54e7", + referralToken: "0x1Eca7AA9ABF2e53E773B4523B6Dc103002d22e7D", + nexusDiscoverCampaign: "0x8a37d1B3a17559F2BC4e6613834b1F13d0A623aC", } as const; /** diff --git a/packages/wallet/src/context/referral/abi/campaign-abis.ts b/packages/wallet/src/context/referral/abi/campaign-abis.ts new file mode 100644 index 000000000..0d91fbc01 --- /dev/null +++ b/packages/wallet/src/context/referral/abi/campaign-abis.ts @@ -0,0 +1,826 @@ +////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// NexusDiscoverCampaign +////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +export const nexusDiscoverCampaignAbi = [ + { + type: "constructor", + inputs: [ + { name: "_token", internalType: "address", type: "address" }, + { name: "_owner", internalType: "address", type: "address" }, + ], + stateMutability: "nonpayable", + }, + { + type: "function", + inputs: [], + name: "cancelOwnershipHandover", + outputs: [], + stateMutability: "payable", + }, + { + type: "function", + inputs: [ + { name: "pendingOwner", internalType: "address", type: "address" }, + ], + name: "completeOwnershipHandover", + outputs: [], + stateMutability: "payable", + }, + { + type: "function", + inputs: [ + { name: "_referee", internalType: "address", type: "address" }, + { name: "_referrer", internalType: "address", type: "address" }, + { name: "_contentId", internalType: "uint256", type: "uint256" }, + ], + name: "distributeContentDiscoveryReward", + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + inputs: [ + { name: "_referee", internalType: "address", type: "address" }, + { name: "_referrer", internalType: "address", type: "address" }, + ], + name: "distributeInstallationReward", + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + inputs: [], + name: "eip712Domain", + outputs: [ + { name: "fields", internalType: "bytes1", type: "bytes1" }, + { name: "name", internalType: "string", type: "string" }, + { name: "version", internalType: "string", type: "string" }, + { name: "chainId", internalType: "uint256", type: "uint256" }, + { + name: "verifyingContract", + internalType: "address", + type: "address", + }, + { name: "salt", internalType: "bytes32", type: "bytes32" }, + { + name: "extensions", + internalType: "uint256[]", + type: "uint256[]", + }, + ], + stateMutability: "view", + }, + { + type: "function", + inputs: [ + { name: "_selector", internalType: "bytes32", type: "bytes32" }, + { name: "_referee", internalType: "address", type: "address" }, + ], + name: "getAllReferrers", + outputs: [ + { + name: "referrerChains", + internalType: "address[]", + type: "address[]", + }, + ], + stateMutability: "view", + }, + { + type: "function", + inputs: [ + { name: "contentId", internalType: "uint256", type: "uint256" }, + ], + name: "getContentDiscoveryTree", + outputs: [{ name: "", internalType: "bytes32", type: "bytes32" }], + stateMutability: "pure", + }, + { + type: "function", + inputs: [{ name: "_user", internalType: "address", type: "address" }], + name: "getPendingAmount", + outputs: [{ name: "", internalType: "uint256", type: "uint256" }], + stateMutability: "view", + }, + { + type: "function", + inputs: [], + name: "getPushPullConfig", + outputs: [ + { + name: "", + internalType: "struct PushPullConfig", + type: "tuple", + components: [ + { name: "token", internalType: "address", type: "address" }, + ], + }, + ], + stateMutability: "view", + }, + { + type: "function", + inputs: [], + name: "getReferralCampaignConfig", + outputs: [ + { + name: "", + internalType: "struct CampaignConfig", + type: "tuple", + components: [ + { + name: "maxLevel", + internalType: "uint256", + type: "uint256", + }, + { + name: "perLevelPercentage", + internalType: "uint256", + type: "uint256", + }, + { name: "token", internalType: "address", type: "address" }, + ], + }, + ], + stateMutability: "view", + }, + { + type: "function", + inputs: [ + { name: "_selector", internalType: "bytes32", type: "bytes32" }, + { name: "_referee", internalType: "address", type: "address" }, + ], + name: "getReferrer", + outputs: [ + { name: "referrer", internalType: "address", type: "address" }, + ], + stateMutability: "view", + }, + { + type: "function", + inputs: [ + { name: "user", internalType: "address", type: "address" }, + { name: "roles", internalType: "uint256", type: "uint256" }, + ], + name: "grantRoles", + outputs: [], + stateMutability: "payable", + }, + { + type: "function", + inputs: [ + { name: "user", internalType: "address", type: "address" }, + { name: "roles", internalType: "uint256", type: "uint256" }, + ], + name: "hasAllRoles", + outputs: [{ name: "", internalType: "bool", type: "bool" }], + stateMutability: "view", + }, + { + type: "function", + inputs: [ + { name: "user", internalType: "address", type: "address" }, + { name: "roles", internalType: "uint256", type: "uint256" }, + ], + name: "hasAnyRole", + outputs: [{ name: "", internalType: "bool", type: "bool" }], + stateMutability: "view", + }, + { + type: "function", + inputs: [], + name: "owner", + outputs: [{ name: "result", internalType: "address", type: "address" }], + stateMutability: "view", + }, + { + type: "function", + inputs: [ + { name: "pendingOwner", internalType: "address", type: "address" }, + ], + name: "ownershipHandoverExpiresAt", + outputs: [{ name: "result", internalType: "uint256", type: "uint256" }], + stateMutability: "view", + }, + { + type: "function", + inputs: [], + name: "pauseCampaign", + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + inputs: [{ name: "_user", internalType: "address", type: "address" }], + name: "pullReward", + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + inputs: [], + name: "pullReward", + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + inputs: [], + name: "renounceOwnership", + outputs: [], + stateMutability: "payable", + }, + { + type: "function", + inputs: [{ name: "roles", internalType: "uint256", type: "uint256" }], + name: "renounceRoles", + outputs: [], + stateMutability: "payable", + }, + { + type: "function", + inputs: [], + name: "requestOwnershipHandover", + outputs: [], + stateMutability: "payable", + }, + { + type: "function", + inputs: [], + name: "resumeCampaign", + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + inputs: [ + { name: "user", internalType: "address", type: "address" }, + { name: "roles", internalType: "uint256", type: "uint256" }, + ], + name: "revokeRoles", + outputs: [], + stateMutability: "payable", + }, + { + type: "function", + inputs: [{ name: "user", internalType: "address", type: "address" }], + name: "rolesOf", + outputs: [{ name: "roles", internalType: "uint256", type: "uint256" }], + stateMutability: "view", + }, + { + type: "function", + inputs: [ + { name: "newOwner", internalType: "address", type: "address" }, + ], + name: "transferOwnership", + outputs: [], + stateMutability: "payable", + }, + { + type: "event", + anonymous: false, + inputs: [ + { + name: "user", + internalType: "address", + type: "address", + indexed: true, + }, + { + name: "contentId", + internalType: "uint256", + type: "uint256", + indexed: false, + }, + ], + name: "ContentDiscoveryAirdropDistributed", + }, + { + type: "event", + anonymous: false, + inputs: [ + { + name: "pendingOwner", + internalType: "address", + type: "address", + indexed: true, + }, + ], + name: "OwnershipHandoverCanceled", + }, + { + type: "event", + anonymous: false, + inputs: [ + { + name: "pendingOwner", + internalType: "address", + type: "address", + indexed: true, + }, + ], + name: "OwnershipHandoverRequested", + }, + { + type: "event", + anonymous: false, + inputs: [ + { + name: "oldOwner", + internalType: "address", + type: "address", + indexed: true, + }, + { + name: "newOwner", + internalType: "address", + type: "address", + indexed: true, + }, + ], + name: "OwnershipTransferred", + }, + { + type: "event", + anonymous: false, + inputs: [ + { + name: "user", + internalType: "address", + type: "address", + indexed: true, + }, + ], + name: "RegistrationAirdropDistributed", + }, + { + type: "event", + anonymous: false, + inputs: [ + { + name: "user", + internalType: "address", + type: "address", + indexed: true, + }, + { + name: "amount", + internalType: "uint256", + type: "uint256", + indexed: false, + }, + ], + name: "RewardAdded", + }, + { + type: "event", + anonymous: false, + inputs: [ + { + name: "user", + internalType: "address", + type: "address", + indexed: true, + }, + { + name: "amount", + internalType: "uint256", + type: "uint256", + indexed: false, + }, + ], + name: "RewardClaimed", + }, + { + type: "event", + anonymous: false, + inputs: [ + { + name: "user", + internalType: "address", + type: "address", + indexed: true, + }, + { + name: "roles", + internalType: "uint256", + type: "uint256", + indexed: true, + }, + ], + name: "RolesUpdated", + }, + { + type: "event", + anonymous: false, + inputs: [ + { + name: "tree", + internalType: "bytes32", + type: "bytes32", + indexed: true, + }, + { + name: "referer", + internalType: "address", + type: "address", + indexed: true, + }, + { + name: "referee", + internalType: "address", + type: "address", + indexed: true, + }, + ], + name: "UserReferred", + }, + { + type: "error", + inputs: [ + { name: "tree", internalType: "bytes32", type: "bytes32" }, + { + name: "currentReferrer", + internalType: "address", + type: "address", + }, + ], + name: "AlreadyHaveReferer", + }, + { + type: "error", + inputs: [{ name: "tree", internalType: "bytes32", type: "bytes32" }], + name: "AlreadyInRefererChain", + }, + { type: "error", inputs: [], name: "AlreadyInitialized" }, + { type: "error", inputs: [], name: "InactiveCampaign" }, + { type: "error", inputs: [], name: "InvalidConfig" }, + { type: "error", inputs: [], name: "NewOwnerIsZeroAddress" }, + { type: "error", inputs: [], name: "NoHandoverRequest" }, + { type: "error", inputs: [], name: "NotEnoughToken" }, + { type: "error", inputs: [], name: "Reentrancy" }, + { type: "error", inputs: [], name: "Unauthorized" }, +] as const; + +////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// ReferralToken +////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +export const referralTokenAbi = [ + { + type: "constructor", + inputs: [{ name: "_owner", internalType: "address", type: "address" }], + stateMutability: "nonpayable", + }, + { + type: "function", + inputs: [], + name: "DOMAIN_SEPARATOR", + outputs: [{ name: "result", internalType: "bytes32", type: "bytes32" }], + stateMutability: "view", + }, + { + type: "function", + inputs: [ + { name: "owner", internalType: "address", type: "address" }, + { name: "spender", internalType: "address", type: "address" }, + ], + name: "allowance", + outputs: [{ name: "result", internalType: "uint256", type: "uint256" }], + stateMutability: "view", + }, + { + type: "function", + inputs: [ + { name: "spender", internalType: "address", type: "address" }, + { name: "amount", internalType: "uint256", type: "uint256" }, + ], + name: "approve", + outputs: [{ name: "", internalType: "bool", type: "bool" }], + stateMutability: "nonpayable", + }, + { + type: "function", + inputs: [{ name: "owner", internalType: "address", type: "address" }], + name: "balanceOf", + outputs: [{ name: "result", internalType: "uint256", type: "uint256" }], + stateMutability: "view", + }, + { + type: "function", + inputs: [], + name: "cancelOwnershipHandover", + outputs: [], + stateMutability: "payable", + }, + { + type: "function", + inputs: [ + { name: "pendingOwner", internalType: "address", type: "address" }, + ], + name: "completeOwnershipHandover", + outputs: [], + stateMutability: "payable", + }, + { + type: "function", + inputs: [], + name: "decimals", + outputs: [{ name: "", internalType: "uint8", type: "uint8" }], + stateMutability: "view", + }, + { + type: "function", + inputs: [ + { name: "user", internalType: "address", type: "address" }, + { name: "roles", internalType: "uint256", type: "uint256" }, + ], + name: "grantRoles", + outputs: [], + stateMutability: "payable", + }, + { + type: "function", + inputs: [ + { name: "user", internalType: "address", type: "address" }, + { name: "roles", internalType: "uint256", type: "uint256" }, + ], + name: "hasAllRoles", + outputs: [{ name: "", internalType: "bool", type: "bool" }], + stateMutability: "view", + }, + { + type: "function", + inputs: [ + { name: "user", internalType: "address", type: "address" }, + { name: "roles", internalType: "uint256", type: "uint256" }, + ], + name: "hasAnyRole", + outputs: [{ name: "", internalType: "bool", type: "bool" }], + stateMutability: "view", + }, + { + type: "function", + inputs: [ + { name: "_to", internalType: "address", type: "address" }, + { name: "_amount", internalType: "uint256", type: "uint256" }, + ], + name: "mint", + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + inputs: [], + name: "name", + outputs: [{ name: "", internalType: "string", type: "string" }], + stateMutability: "pure", + }, + { + type: "function", + inputs: [{ name: "owner", internalType: "address", type: "address" }], + name: "nonces", + outputs: [{ name: "result", internalType: "uint256", type: "uint256" }], + stateMutability: "view", + }, + { + type: "function", + inputs: [], + name: "owner", + outputs: [{ name: "result", internalType: "address", type: "address" }], + stateMutability: "view", + }, + { + type: "function", + inputs: [ + { name: "pendingOwner", internalType: "address", type: "address" }, + ], + name: "ownershipHandoverExpiresAt", + outputs: [{ name: "result", internalType: "uint256", type: "uint256" }], + stateMutability: "view", + }, + { + type: "function", + inputs: [ + { name: "owner", internalType: "address", type: "address" }, + { name: "spender", internalType: "address", type: "address" }, + { name: "value", internalType: "uint256", type: "uint256" }, + { name: "deadline", internalType: "uint256", type: "uint256" }, + { name: "v", internalType: "uint8", type: "uint8" }, + { name: "r", internalType: "bytes32", type: "bytes32" }, + { name: "s", internalType: "bytes32", type: "bytes32" }, + ], + name: "permit", + outputs: [], + stateMutability: "nonpayable", + }, + { + type: "function", + inputs: [], + name: "renounceOwnership", + outputs: [], + stateMutability: "payable", + }, + { + type: "function", + inputs: [{ name: "roles", internalType: "uint256", type: "uint256" }], + name: "renounceRoles", + outputs: [], + stateMutability: "payable", + }, + { + type: "function", + inputs: [], + name: "requestOwnershipHandover", + outputs: [], + stateMutability: "payable", + }, + { + type: "function", + inputs: [ + { name: "user", internalType: "address", type: "address" }, + { name: "roles", internalType: "uint256", type: "uint256" }, + ], + name: "revokeRoles", + outputs: [], + stateMutability: "payable", + }, + { + type: "function", + inputs: [{ name: "user", internalType: "address", type: "address" }], + name: "rolesOf", + outputs: [{ name: "roles", internalType: "uint256", type: "uint256" }], + stateMutability: "view", + }, + { + type: "function", + inputs: [], + name: "symbol", + outputs: [{ name: "", internalType: "string", type: "string" }], + stateMutability: "pure", + }, + { + type: "function", + inputs: [], + name: "totalSupply", + outputs: [{ name: "result", internalType: "uint256", type: "uint256" }], + stateMutability: "view", + }, + { + type: "function", + inputs: [ + { name: "to", internalType: "address", type: "address" }, + { name: "amount", internalType: "uint256", type: "uint256" }, + ], + name: "transfer", + outputs: [{ name: "", internalType: "bool", type: "bool" }], + stateMutability: "nonpayable", + }, + { + type: "function", + inputs: [ + { name: "from", internalType: "address", type: "address" }, + { name: "to", internalType: "address", type: "address" }, + { name: "amount", internalType: "uint256", type: "uint256" }, + ], + name: "transferFrom", + outputs: [{ name: "", internalType: "bool", type: "bool" }], + stateMutability: "nonpayable", + }, + { + type: "function", + inputs: [ + { name: "newOwner", internalType: "address", type: "address" }, + ], + name: "transferOwnership", + outputs: [], + stateMutability: "payable", + }, + { + type: "event", + anonymous: false, + inputs: [ + { + name: "owner", + internalType: "address", + type: "address", + indexed: true, + }, + { + name: "spender", + internalType: "address", + type: "address", + indexed: true, + }, + { + name: "amount", + internalType: "uint256", + type: "uint256", + indexed: false, + }, + ], + name: "Approval", + }, + { + type: "event", + anonymous: false, + inputs: [ + { + name: "pendingOwner", + internalType: "address", + type: "address", + indexed: true, + }, + ], + name: "OwnershipHandoverCanceled", + }, + { + type: "event", + anonymous: false, + inputs: [ + { + name: "pendingOwner", + internalType: "address", + type: "address", + indexed: true, + }, + ], + name: "OwnershipHandoverRequested", + }, + { + type: "event", + anonymous: false, + inputs: [ + { + name: "oldOwner", + internalType: "address", + type: "address", + indexed: true, + }, + { + name: "newOwner", + internalType: "address", + type: "address", + indexed: true, + }, + ], + name: "OwnershipTransferred", + }, + { + type: "event", + anonymous: false, + inputs: [ + { + name: "user", + internalType: "address", + type: "address", + indexed: true, + }, + { + name: "roles", + internalType: "uint256", + type: "uint256", + indexed: true, + }, + ], + name: "RolesUpdated", + }, + { + type: "event", + anonymous: false, + inputs: [ + { + name: "from", + internalType: "address", + type: "address", + indexed: true, + }, + { + name: "to", + internalType: "address", + type: "address", + indexed: true, + }, + { + name: "amount", + internalType: "uint256", + type: "uint256", + indexed: false, + }, + ], + name: "Transfer", + }, + { type: "error", inputs: [], name: "AllowanceOverflow" }, + { type: "error", inputs: [], name: "AllowanceUnderflow" }, + { type: "error", inputs: [], name: "AlreadyInitialized" }, + { type: "error", inputs: [], name: "InsufficientAllowance" }, + { type: "error", inputs: [], name: "InsufficientBalance" }, + { type: "error", inputs: [], name: "InvalidPermit" }, + { type: "error", inputs: [], name: "NewOwnerIsZeroAddress" }, + { type: "error", inputs: [], name: "NoHandoverRequest" }, + { type: "error", inputs: [], name: "PermitExpired" }, + { type: "error", inputs: [], name: "TotalSupplyOverflow" }, + { type: "error", inputs: [], name: "Unauthorized" }, +] as const; diff --git a/packages/wallet/src/context/referral/action/userReferred.ts b/packages/wallet/src/context/referral/action/userReferred.ts new file mode 100644 index 000000000..567dbcb73 --- /dev/null +++ b/packages/wallet/src/context/referral/action/userReferred.ts @@ -0,0 +1,51 @@ +"use server"; + +import { addresses } from "@/context/common/blockchain/addresses"; +import { frakChainPocClient } from "@/context/common/blockchain/provider"; +import { nexusDiscoverCampaignAbi } from "@/context/referral/abi/campaign-abis"; +import { tryit } from "radash"; +import { type Address, type Hex, encodeFunctionData } from "viem"; +import { privateKeyToAccount } from "viem/accounts"; +import { prepareTransactionRequest, sendTransaction } from "viem/actions"; +import { arbitrumSepolia } from "viem/chains"; + +/** + * Tell that a user has been referred by another user + * @param user + * @param referrer + */ +export async function setUserReferred({ + user, + referrer, +}: { user: Address; referrer: Address }) { + // Get the airdropper private key + const airdropperPrivateKey = process.env.AIRDROP_PRIVATE_KEY; + if (!airdropperPrivateKey) { + throw new Error("Missing AIRDROP_PRIVATE_KEY env variable"); + } + + // Build our airdrop account + const airdropperAccount = privateKeyToAccount(airdropperPrivateKey as Hex); + + // Prepare the transaction + const [, preparationResult] = await tryit(() => + prepareTransactionRequest(frakChainPocClient, { + account: airdropperAccount, + chain: arbitrumSepolia, + to: addresses.nexusDiscoverCampaign, + data: encodeFunctionData({ + abi: nexusDiscoverCampaignAbi, + functionName: "distributeInstallationReward", + args: [user, referrer], + }), + }) + )(); + if (!preparationResult) { + return undefined; + } + + // Send the tx + const txHash = await sendTransaction(frakChainPocClient, preparationResult); + console.log("User referred by another user", { user, referrer, txHash }); + return txHash; +} diff --git a/packages/wallet/src/context/referral/action/userReferredOnContent.ts b/packages/wallet/src/context/referral/action/userReferredOnContent.ts new file mode 100644 index 000000000..ac23bb207 --- /dev/null +++ b/packages/wallet/src/context/referral/action/userReferredOnContent.ts @@ -0,0 +1,58 @@ +"use server"; + +import { addresses } from "@/context/common/blockchain/addresses"; +import { frakChainPocClient } from "@/context/common/blockchain/provider"; +import { nexusDiscoverCampaignAbi } from "@/context/referral/abi/campaign-abis"; +import { tryit } from "radash"; +import { type Address, type Hex, encodeFunctionData } from "viem"; +import { privateKeyToAccount } from "viem/accounts"; +import { prepareTransactionRequest, sendTransaction } from "viem/actions"; +import { arbitrumSepolia } from "viem/chains"; + +/** + * Tell that a user has been referred by another user on a given content + * @param user + * @param referrer + * @param contentId + */ +export async function setUserReferredOnContent({ + user, + referrer, + contentId, +}: { user: Address; referrer: Address; contentId: Hex }) { + // Get the airdropper private key + const airdropperPrivateKey = process.env.AIRDROP_PRIVATE_KEY; + if (!airdropperPrivateKey) { + throw new Error("Missing AIRDROP_PRIVATE_KEY env variable"); + } + + // Build our airdrop account + const airdropperAccount = privateKeyToAccount(airdropperPrivateKey as Hex); + + // Prepare the transaction + const [, preparationResult] = await tryit(() => + prepareTransactionRequest(frakChainPocClient, { + account: airdropperAccount, + chain: arbitrumSepolia, + to: addresses.nexusDiscoverCampaign, + data: encodeFunctionData({ + abi: nexusDiscoverCampaignAbi, + functionName: "distributeContentDiscoveryReward", + args: [user, referrer, BigInt(contentId)], + }), + }) + )(); + if (!preparationResult) { + return undefined; + } + + // Send the tx + const txHash = await sendTransaction(frakChainPocClient, preparationResult); + console.log("User referred by another user on the given content", { + user, + referrer, + contentId, + txHash, + }); + return txHash; +} From 74bb6f72b2b048b3a8ceaaf00731a9052f8d41f5 Mon Sep 17 00:00:00 2001 From: Rodolphe Stoclin Date: Thu, 2 May 2024 18:39:24 +0200 Subject: [PATCH 02/17] =?UTF-8?q?=E2=9C=A8=20Add=20useNexusReferral=20hook?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../module/article/component/Read/index.tsx | 4 +++ packages/sdk/src/react/hook/index.ts | 1 + packages/sdk/src/react/hook/useMounted.ts | 11 ++++++ .../sdk/src/react/hook/useNexusReferral.ts | 23 ++++++++++++ .../sdk/src/react/hook/useWindowLocation.ts | 36 +++++++++++++++++++ packages/sdk/src/react/index.ts | 1 + 6 files changed, 76 insertions(+) create mode 100644 packages/sdk/src/react/hook/useMounted.ts create mode 100644 packages/sdk/src/react/hook/useNexusReferral.ts create mode 100644 packages/sdk/src/react/hook/useWindowLocation.ts diff --git a/packages/example/src/module/article/component/Read/index.tsx b/packages/example/src/module/article/component/Read/index.tsx index 0e0052f20..5fa57bee8 100644 --- a/packages/example/src/module/article/component/Read/index.tsx +++ b/packages/example/src/module/article/component/Read/index.tsx @@ -5,6 +5,7 @@ import type { Article } from "@/type/Article"; import { useArticleUnlockOptions, useArticleUnlockStatus, + useNexusReferral, useWalletStatus, } from "@frak-labs/nexus-sdk/react"; import { useEffect, useState } from "react"; @@ -24,6 +25,9 @@ export function ReadArticle({ // The iframe reference const [iframeRef, setIframeRef] = useState(null); + // The nexus referral + useNexusReferral(); + // The unlock options for the article const { data: unlockOptions } = useArticleUnlockOptions({ articleId: article.id as Hex, diff --git a/packages/sdk/src/react/hook/index.ts b/packages/sdk/src/react/hook/index.ts index 528e0469d..7cf3fac2f 100644 --- a/packages/sdk/src/react/hook/index.ts +++ b/packages/sdk/src/react/hook/index.ts @@ -5,3 +5,4 @@ export { useWalletStatus } from "./useWalletStatus"; export type { WalletStatusQueryReturnType } from "./useWalletStatus"; export { useArticleUnlockStatus } from "./useArticleUnlockStatus"; export type { ArticleUnlockStatusQueryReturnType } from "./useArticleUnlockStatus"; +export { useNexusReferral } from "./useNexusReferral"; diff --git a/packages/sdk/src/react/hook/useMounted.ts b/packages/sdk/src/react/hook/useMounted.ts new file mode 100644 index 000000000..e72da5478 --- /dev/null +++ b/packages/sdk/src/react/hook/useMounted.ts @@ -0,0 +1,11 @@ +import { useEffect, useState } from "react"; + +export function useMounted() { + const [mounted, setMounted] = useState(false); + + useEffect(() => { + setMounted(true); + }, []); + + return mounted; +} diff --git a/packages/sdk/src/react/hook/useNexusReferral.ts b/packages/sdk/src/react/hook/useNexusReferral.ts new file mode 100644 index 000000000..7f595e16e --- /dev/null +++ b/packages/sdk/src/react/hook/useNexusReferral.ts @@ -0,0 +1,23 @@ +import { useEffect } from "react"; +import { useWalletStatus } from "./useWalletStatus"; +import { useWindowLocation } from "./useWindowLocation"; + +/** + * Use the current nexus referral + */ +export function useNexusReferral() { + const { href } = useWindowLocation(); + const { data: walletStatus } = useWalletStatus(); + + useEffect(() => { + if (!href || walletStatus?.key !== "connected") return; + + const url = new URL(href); + const context = url.searchParams.get("nexusContext"); + + if (!context) { + url.searchParams.set("nexusContext", walletStatus?.wallet); + window.history.replaceState(null, "", url.toString()); + } + }, [href, walletStatus]); +} diff --git a/packages/sdk/src/react/hook/useWindowLocation.ts b/packages/sdk/src/react/hook/useWindowLocation.ts new file mode 100644 index 000000000..73f438cba --- /dev/null +++ b/packages/sdk/src/react/hook/useWindowLocation.ts @@ -0,0 +1,36 @@ +import { useEffect, useState } from "react"; +import { useMounted } from "./useMounted"; + +export const useWindowLocation = (): { + location: Location | undefined; + href: string | undefined; +} => { + const isMounted = useMounted(); + const [location, setLocation] = useState( + isMounted ? window.location : undefined + ); + const [href, setHref] = useState( + isMounted ? window.location.href : undefined + ); + + useEffect(() => { + if (!isMounted) return; + + const setWindowLocation = () => { + setLocation(window.location); + setHref(window.location.href); + }; + + if (!location) { + setWindowLocation(); + } + + window.addEventListener("popstate", setWindowLocation); + + return () => { + window.removeEventListener("popstate", setWindowLocation); + }; + }, [isMounted, location]); + + return { location, href }; +}; diff --git a/packages/sdk/src/react/index.ts b/packages/sdk/src/react/index.ts index 9c64da1ab..eeac4d6a4 100644 --- a/packages/sdk/src/react/index.ts +++ b/packages/sdk/src/react/index.ts @@ -17,6 +17,7 @@ export { useArticleUnlockOptions, useWalletStatus, useArticleUnlockStatus, + useNexusReferral, } from "./hook"; export type { WalletStatusQueryReturnType, From a02303a04978f408e4fcbb0ab6d20c7bdc644e65 Mon Sep 17 00:00:00 2001 From: KONFeature Date: Thu, 2 May 2024 20:44:44 +0200 Subject: [PATCH 03/17] =?UTF-8?q?=E2=9C=A8=20Add=20get=20pending=20reward?= =?UTF-8?q?=20action?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../context/history/action/fetchHistory.ts | 2 +- .../referral/action/pendingReferral.ts | 30 +++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 packages/wallet/src/context/referral/action/pendingReferral.ts diff --git a/packages/wallet/src/context/history/action/fetchHistory.ts b/packages/wallet/src/context/history/action/fetchHistory.ts index 4150da13a..0079e7f6a 100644 --- a/packages/wallet/src/context/history/action/fetchHistory.ts +++ b/packages/wallet/src/context/history/action/fetchHistory.ts @@ -86,7 +86,7 @@ async function _fetchWalletHistory({ // Get the frk received or sent events for a user function getFrkEvents(args: { to?: Address; from?: Address }) { return getLogs(viemClient, { - address: addresses.paywallToken, + address: [addresses.paywallToken, addresses.referralToken], event: frkTransferEvent, args, strict: true, diff --git a/packages/wallet/src/context/referral/action/pendingReferral.ts b/packages/wallet/src/context/referral/action/pendingReferral.ts new file mode 100644 index 000000000..77d6a8e9b --- /dev/null +++ b/packages/wallet/src/context/referral/action/pendingReferral.ts @@ -0,0 +1,30 @@ +"use server"; + +import { addresses } from "@/context/common/blockchain/addresses"; +import { frakChainPocClient } from "@/context/common/blockchain/provider"; +import { nexusDiscoverCampaignAbi } from "@/context/referral/abi/campaign-abis"; +import { type Address, formatEther } from "viem"; +import { readContract } from "viem/actions"; + +/** + * Tell that a user has been referred by another user + * @param user + * @param referrer + */ +export async function getPendingWalletReferralReward({ + user, +}: { user: Address }) { + const pendingAmount = await readContract(frakChainPocClient, { + address: addresses.nexusDiscoverCampaign, + abi: nexusDiscoverCampaignAbi, + functionName: "getPendingAmount", + args: [user], + }); + if (!pendingAmount) { + return undefined; + } + return { + rFrkPendingRaw: pendingAmount, + rFrkPendingFormatted: formatEther(pendingAmount), + }; +} From 7b8cb56c4768f0531921bc963ca00170c9a19f0c Mon Sep 17 00:00:00 2001 From: Rodolphe Stoclin Date: Fri, 3 May 2024 16:15:15 +0200 Subject: [PATCH 04/17] =?UTF-8?q?=E2=9C=A8=20Add=20referral=20flow=20when?= =?UTF-8?q?=20a=20user=20is=20connected?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../module/article/component/Read/index.tsx | 4 +- packages/sdk/src/core/actions/index.ts | 1 + .../sdk/src/core/actions/setUserReferred.ts | 31 ++++++++++ packages/sdk/src/core/index.ts | 2 + packages/sdk/src/core/types/index.ts | 4 ++ packages/sdk/src/core/types/rpc.ts | 11 +++- .../sdk/src/core/types/rpc/setUserReferred.ts | 28 +++++++++ .../utils/compression/iframeRpcKeyProvider.ts | 26 ++++++-- .../sdk/src/react/hook/useNexusReferral.ts | 61 ++++++++++++++++++- .../sdk/utils/iFrameRequestResolver.ts | 14 +++++ .../src/module/listener/component/index.tsx | 10 +++ .../hooks/useSetUserReferredListener.ts | 60 ++++++++++++++++++ 12 files changed, 242 insertions(+), 10 deletions(-) create mode 100644 packages/sdk/src/core/actions/setUserReferred.ts create mode 100644 packages/sdk/src/core/types/rpc/setUserReferred.ts create mode 100644 packages/wallet/src/module/listener/hooks/useSetUserReferredListener.ts diff --git a/packages/example/src/module/article/component/Read/index.tsx b/packages/example/src/module/article/component/Read/index.tsx index 5fa57bee8..2a65dbc22 100644 --- a/packages/example/src/module/article/component/Read/index.tsx +++ b/packages/example/src/module/article/component/Read/index.tsx @@ -26,7 +26,9 @@ export function ReadArticle({ const [iframeRef, setIframeRef] = useState(null); // The nexus referral - useNexusReferral(); + useNexusReferral({ + contentId: article.contentId as Hex, + }); // The unlock options for the article const { data: unlockOptions } = useArticleUnlockOptions({ diff --git a/packages/sdk/src/core/actions/index.ts b/packages/sdk/src/core/actions/index.ts index 87ac9ae4a..e9b39c097 100644 --- a/packages/sdk/src/core/actions/index.ts +++ b/packages/sdk/src/core/actions/index.ts @@ -7,3 +7,4 @@ export { getStartArticleUnlockUrl, decodeStartUnlockReturn, } from "./startUnlock"; +export { setUserReferred } from "./setUserReferred"; diff --git a/packages/sdk/src/core/actions/setUserReferred.ts b/packages/sdk/src/core/actions/setUserReferred.ts new file mode 100644 index 000000000..31d5114ca --- /dev/null +++ b/packages/sdk/src/core/actions/setUserReferred.ts @@ -0,0 +1,31 @@ +import type { Address, Hex } from "viem"; +import type { NexusClient, SetUserReferredReturnType } from "../types"; + +/** + * Type used to get the user referred options + */ +export type SetUserReferredParams = { + contentId: Hex; + walletAddress: Address; +}; + +/** + * Function used to watch a current user referred + * @param client + * @param contentId + * @param walletAddress + * @param callback + */ +export function setUserReferred( + client: NexusClient, + { contentId, walletAddress }: SetUserReferredParams, + callback: (status: SetUserReferredReturnType) => void +) { + return client.listenerRequest( + { + method: "frak_listenToSetUserReferred", + params: [contentId, walletAddress], + }, + callback + ); +} diff --git a/packages/sdk/src/core/index.ts b/packages/sdk/src/core/index.ts index bceb6b7ab..dcd284e21 100644 --- a/packages/sdk/src/core/index.ts +++ b/packages/sdk/src/core/index.ts @@ -22,6 +22,8 @@ export type { RedirectRpcSchema, StartArticleUnlockParams, StartArticleUnlockReturnType, + SetUserReferredParams, + SetUserReferredReturnType, // Client NexusClient, // Transport diff --git a/packages/sdk/src/core/types/index.ts b/packages/sdk/src/core/types/index.ts index 623f80f22..1f60926fe 100644 --- a/packages/sdk/src/core/types/index.ts +++ b/packages/sdk/src/core/types/index.ts @@ -6,6 +6,10 @@ export type { StartArticleUnlockParams, StartArticleUnlockReturnType, } from "./rpc/startUnlock"; +export type { + SetUserReferredParams, + SetUserReferredReturnType, +} from "./rpc/setUserReferred"; export type { IFrameRpcSchema, RedirectRpcSchema } from "./rpc"; // Client related export type { NexusClient } from "./client"; diff --git a/packages/sdk/src/core/types/rpc.ts b/packages/sdk/src/core/types/rpc.ts index c6b3fdb1f..66e2daff1 100644 --- a/packages/sdk/src/core/types/rpc.ts +++ b/packages/sdk/src/core/types/rpc.ts @@ -1,4 +1,5 @@ -import type { Hex } from "viem"; +import type { Address, Hex } from "viem"; +import type { SetUserReferredReturnType } from "./rpc/setUserReferred"; import type { StartArticleUnlockParams, StartArticleUnlockReturnType, @@ -35,6 +36,14 @@ export type IFrameRpcSchema = [ Parameters: [contentId: Hex, articleId: Hex]; ReturnType: ArticleUnlockStatusReturnType; }, + /** + * Method used to set the referred user + */ + { + Method: "frak_listenToSetUserReferred"; + Parameters: [contentId: Hex, walletAddress: Address]; + ReturnType: SetUserReferredReturnType; + }, ]; /** diff --git a/packages/sdk/src/core/types/rpc/setUserReferred.ts b/packages/sdk/src/core/types/rpc/setUserReferred.ts new file mode 100644 index 000000000..82aab2492 --- /dev/null +++ b/packages/sdk/src/core/types/rpc/setUserReferred.ts @@ -0,0 +1,28 @@ +import type { Hex } from "viem"; + +/** + * Parameters of the referred request + */ +export type SetUserReferredParams = Readonly<{ + contentId: Hex; +}>; + +/** + * Return type of the referred request + */ +export type SetUserReferredReturnType = + | UserConnected + | UserNotConnected + | UserIsSameWallet; + +type UserConnected = { + key: "connected"; +}; + +type UserNotConnected = { + key: "not-connected"; +}; + +type UserIsSameWallet = { + key: "same-wallet"; +}; diff --git a/packages/sdk/src/core/utils/compression/iframeRpcKeyProvider.ts b/packages/sdk/src/core/utils/compression/iframeRpcKeyProvider.ts index ef3c20d1f..da0b669dc 100644 --- a/packages/sdk/src/core/utils/compression/iframeRpcKeyProvider.ts +++ b/packages/sdk/src/core/utils/compression/iframeRpcKeyProvider.ts @@ -1,12 +1,13 @@ -import type { KeyProvider } from "../../types/compression"; -import type { IFrameRpcSchema } from "../../types/rpc"; -import type { UnlockOptionsReturnType } from "../../types/rpc/unlockOption"; -import type { ArticleUnlockStatusReturnType } from "../../types/rpc/unlockStatus"; -import type { WalletStatusReturnType } from "../../types/rpc/walletStatus"; import type { + ArticleUnlockStatusReturnType, ExtractedParametersFromRpc, ExtractedReturnTypeFromRpc, -} from "../../types/transport"; + IFrameRpcSchema, + KeyProvider, + SetUserReferredReturnType, + UnlockOptionsReturnType, + WalletStatusReturnType, +} from "../../types"; /** * Get the right request key provider for the given args @@ -30,6 +31,11 @@ export const iFrameRequestKeyProvider: KeyProvider< return ["article-unlock-status", args.params[0], args.params[1]]; } + // Referred user key + if (args.method === "frak_listenToSetUserReferred") { + return ["user-referred", args.params[0], args.params[1]]; + } + // Not found throw new Error(`No key provider found for the arguments ${args}`); }; @@ -87,5 +93,13 @@ export function getIFrameResponseKeyProvider< ]) as RpcResponseKeyProvider; } + // Referred user key + if (param.method === "frak_listenToSetUserReferred") { + return ((response: SetUserReferredReturnType) => [ + "user-referred", + response.key, + ]) as RpcResponseKeyProvider; + } + throw new Error(`No key provider found for the request ${param}`); } diff --git a/packages/sdk/src/react/hook/useNexusReferral.ts b/packages/sdk/src/react/hook/useNexusReferral.ts index 7f595e16e..d28136d3a 100644 --- a/packages/sdk/src/react/hook/useNexusReferral.ts +++ b/packages/sdk/src/react/hook/useNexusReferral.ts @@ -1,13 +1,40 @@ -import { useEffect } from "react"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; +import { useCallback, useEffect, useState } from "react"; +import type { Address } from "viem"; +import type { + SetUserReferredParams, + SetUserReferredReturnType, +} from "../../core"; +import { setUserReferred } from "../../core/actions"; +import { useNexusClient } from "./useNexusClient"; import { useWalletStatus } from "./useWalletStatus"; import { useWindowLocation } from "./useWindowLocation"; +export type SetUserReferredQueryReturnType = + | SetUserReferredReturnType + | { + key: "waiting-response"; + }; + /** * Use the current nexus referral */ -export function useNexusReferral() { +export function useNexusReferral({ contentId }: SetUserReferredParams) { const { href } = useWindowLocation(); + const queryClient = useQueryClient(); + const client = useNexusClient(); const { data: walletStatus } = useWalletStatus(); + const [walletAddress, setWalletAddress] = useState
(); + + const newStatusUpdated = useCallback( + (event: SetUserReferredReturnType) => { + queryClient.setQueryData( + ["setUserReferredQueryReturnTypeListener"], + event + ); + }, + [queryClient] + ); useEffect(() => { if (!href || walletStatus?.key !== "connected") return; @@ -19,5 +46,35 @@ export function useNexusReferral() { url.searchParams.set("nexusContext", walletStatus?.wallet); window.history.replaceState(null, "", url.toString()); } + + if (context) { + setWalletAddress(context as Address); + } }, [href, walletStatus]); + + return useQuery({ + gcTime: 0, + queryKey: ["setUserReferredQueryReturnTypeListener"], + queryFn: async () => { + if (!(contentId && walletAddress)) + return { key: "waiting-response" }; + + if ( + walletStatus?.key === "connected" && + walletStatus?.wallet === walletAddress + ) + return { key: "same-wallet" }; + + // Setup the listener + await setUserReferred( + client, + { contentId, walletAddress }, + newStatusUpdated + ); + + // Wait for the first response + return { key: "waiting-response" }; + }, + enabled: !!contentId && !!walletAddress, + }); } diff --git a/packages/wallet/src/context/sdk/utils/iFrameRequestResolver.ts b/packages/wallet/src/context/sdk/utils/iFrameRequestResolver.ts index ca724a04f..134e52361 100644 --- a/packages/wallet/src/context/sdk/utils/iFrameRequestResolver.ts +++ b/packages/wallet/src/context/sdk/utils/iFrameRequestResolver.ts @@ -172,5 +172,19 @@ export function getIframeRequestKeyProvider( ]) as KeyProvider>; } + // Referred user key + if (event.topic === "frak_listenToSetUserReferred") { + return (( + request: Extract< + ExtractedParametersFromRpc, + { method: "frak_listenToSetUserReferred" } + > + ) => [ + "user-referred", + request.params[0], + request.params[1], + ]) as KeyProvider>; + } + throw new Error(`No key provider found for the event ${event}`); } diff --git a/packages/wallet/src/module/listener/component/index.tsx b/packages/wallet/src/module/listener/component/index.tsx index bf5e4e767..f73cc48d0 100644 --- a/packages/wallet/src/module/listener/component/index.tsx +++ b/packages/wallet/src/module/listener/component/index.tsx @@ -3,6 +3,7 @@ import { createIFrameRequestResolver } from "@/context/sdk/utils/iFrameRequestResolver"; import { useArticleUnlockStatusListener } from "@/module/listener/hooks/useArticleUnlockStatusListener"; import { useGetArticleUnlockOptionsListener } from "@/module/listener/hooks/useGetArticleUnlockOptionsListener"; +import { useSetUserReferredListener } from "@/module/listener/hooks/useSetUserReferredListener"; import { useWalletStatusListener } from "@/module/listener/hooks/useWalletStatusListener"; import { useEffect, useState } from "react"; @@ -26,6 +27,9 @@ export function ListenerUI() { const { onArticleUnlockStatusListenerRequest } = useArticleUnlockStatusListener(); + // Hook used when a user referred is requested + const { onUserReferredListenRequest } = useSetUserReferredListener(); + // Create the resolver useEffect(() => { const newResolver = createIFrameRequestResolver({ @@ -46,6 +50,11 @@ export function ListenerUI() { * @param emitter */ frak_getArticleUnlockOptions: onGetArticleUnlockOptions, + + /** + * Listen request on the user referred + */ + frak_listenToSetUserReferred: onUserReferredListenRequest, }); // Set our new resolver @@ -59,6 +68,7 @@ export function ListenerUI() { onWalletListenRequest, onGetArticleUnlockOptions, onArticleUnlockStatusListenerRequest, + onUserReferredListenRequest, ]); /** diff --git a/packages/wallet/src/module/listener/hooks/useSetUserReferredListener.ts b/packages/wallet/src/module/listener/hooks/useSetUserReferredListener.ts new file mode 100644 index 000000000..a6a3c6172 --- /dev/null +++ b/packages/wallet/src/module/listener/hooks/useSetUserReferredListener.ts @@ -0,0 +1,60 @@ +import { setUserReferredOnContent } from "@/context/referral/action/userReferredOnContent"; +import type { IFrameRequestResolver } from "@/context/sdk/utils/iFrameRequestResolver"; +import { sessionAtom } from "@/module/common/atoms/session"; +import type { + ExtractedParametersFromRpc, + IFrameRpcSchema, +} from "@frak-labs/nexus-sdk/core"; +import { useAtomValue } from "jotai"; +import { useCallback } from "react"; + +type OnListenToUserReferred = IFrameRequestResolver< + Extract< + ExtractedParametersFromRpc, + { method: "frak_listenToSetUserReferred" } + > +>; + +/** + * Hook use to listen to the user referred + */ +export function useSetUserReferredListener() { + /** + * Get the current user session + */ + const session = useAtomValue(sessionAtom); + + /** + * The function that will be called when a user referred is requested + * @param request + * @param emitter + */ + const onUserReferredListenRequest: OnListenToUserReferred = useCallback( + async (request, emitter) => { + // Extract the contentId and walletAddress + const contentId = request.params[0]; + const walletAddress = request.params[1]; + + // If no contentId or articleId, return + if (!(contentId && walletAddress && session?.wallet?.address)) { + return; + } + + await setUserReferredOnContent({ + user: session?.wallet?.address, + referrer: walletAddress, + contentId, + }); + + // Send the response + await emitter({ + key: "connected", + }); + }, + [session?.wallet?.address] + ); + + return { + onUserReferredListenRequest, + }; +} From b591174f466f33c5bc4553acea77ed1abe245796 Mon Sep 17 00:00:00 2001 From: Rodolphe Stoclin Date: Mon, 6 May 2024 10:57:38 +0200 Subject: [PATCH 05/17] =?UTF-8?q?=E2=9C=A8=20Add=20referral=20flow=20when?= =?UTF-8?q?=20a=20user=20is=20not=20connected?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../article/component/Popup/index.module.css | 39 ++++++++++ .../module/article/component/Popup/index.tsx | 37 ++++++++++ .../component/Read/InjectBannerComponent.tsx | 2 +- .../component/Read/InjectPopupComponent.tsx | 71 +++++++++++++++++++ .../module/article/component/Read/index.tsx | 6 +- .../sdk/src/core/types/rpc/setUserReferred.ts | 12 +++- .../sdk/src/react/hook/useNexusReferral.ts | 6 +- .../module/listener/atoms/referralHistory.ts | 19 +++++ .../hooks/useSetUserReferredListener.ts | 48 +++++++++---- 9 files changed, 223 insertions(+), 17 deletions(-) create mode 100644 packages/example/src/module/article/component/Popup/index.module.css create mode 100644 packages/example/src/module/article/component/Popup/index.tsx create mode 100644 packages/example/src/module/article/component/Read/InjectPopupComponent.tsx create mode 100644 packages/wallet/src/module/listener/atoms/referralHistory.ts diff --git a/packages/example/src/module/article/component/Popup/index.module.css b/packages/example/src/module/article/component/Popup/index.module.css new file mode 100644 index 000000000..92ca8ceb5 --- /dev/null +++ b/packages/example/src/module/article/component/Popup/index.module.css @@ -0,0 +1,39 @@ +.popup { + align-items: center; + color: #383f4e; + display: flex; + justify-content: center; + left: 0; + overflow: hidden; + position: fixed; + scroll-behavior: smooth; + bottom: 0; + width: 100%; + z-index: 10000001; + font-size: 16px; + line-height: 24px; +} + +.popup__content { + background-color: #fff; + display: flex; + flex-direction: column; + max-height: calc(100% - 80px); + max-width: 90%; + overflow: visible; + margin: 10px; + padding: 10px; + position: relative; + width: 100%; + border-radius: 10px; + border: 1px solid #e5e5e5; +} + +.popup__explanation { + color: #2a303b; + font-size: 18px; + line-height: 24px; + margin-bottom: 20px; + font-weight: 600; + text-align: center; +} diff --git a/packages/example/src/module/article/component/Popup/index.tsx b/packages/example/src/module/article/component/Popup/index.tsx new file mode 100644 index 000000000..6874fd32e --- /dev/null +++ b/packages/example/src/module/article/component/Popup/index.tsx @@ -0,0 +1,37 @@ +import css from "!!raw-loader!./index.module.css"; +import { frakWalletSdkConfig } from "@/context/frak-wallet/config"; +import { Button } from "@/module/common/component/Button"; +import type { Article } from "@/type/Article"; + +export const cssRaw = css; + +function buildRedirectUrl(redirectUrl: string) { + const outputUrl = new URL(frakWalletSdkConfig.walletUrl); + outputUrl.pathname = "/register"; + outputUrl.searchParams.set("redirectUrl", encodeURIComponent(redirectUrl)); + return outputUrl.toString(); +} + +export function Popup({ article }: { article: Article }) { + return ( +
+
+

+ A Nexus user shared this link with you, create a Nexus + account to instantly get 50rFRK +

+ +
+
+ ); +} diff --git a/packages/example/src/module/article/component/Read/InjectBannerComponent.tsx b/packages/example/src/module/article/component/Read/InjectBannerComponent.tsx index f120b671a..b1170a40d 100644 --- a/packages/example/src/module/article/component/Read/InjectBannerComponent.tsx +++ b/packages/example/src/module/article/component/Read/InjectBannerComponent.tsx @@ -20,7 +20,7 @@ export function InjectBannerComponent({ const articleIframeDocument = articleIframe?.contentWindow?.document; useEffect(() => { - const containerName = "frak-paywall"; + const containerName = "frak-banner"; let containerRoot = articleIframeDocument?.getElementById(containerName); diff --git a/packages/example/src/module/article/component/Read/InjectPopupComponent.tsx b/packages/example/src/module/article/component/Read/InjectPopupComponent.tsx new file mode 100644 index 000000000..31bbaa48a --- /dev/null +++ b/packages/example/src/module/article/component/Read/InjectPopupComponent.tsx @@ -0,0 +1,71 @@ +import { Popup } from "@/module/article/component/Popup"; +import { cssRaw as cssRawButton } from "@/module/common/component/Button"; +import type { Article } from "@/type/Article"; +import { useEffect, useState } from "react"; +import { createPortal } from "react-dom"; +import { cssRaw } from "../Popup"; + +export function InjectPopupComponent({ + article, +}: { + article: Article; +}) { + const [containerRoot, setContainerRoot] = useState(); + + // Get the article's iframe + const articleIframeName = "frak-article-iframe"; + const articleIframe = document.querySelector( + `#${articleIframeName}` + ) as HTMLIFrameElement; + const articleIframeDocument = articleIframe?.contentWindow?.document; + + useEffect(() => { + const containerName = "frak-popup"; + let containerRoot = + articleIframeDocument?.getElementById(containerName); + + // If the container does not exist, create it + if (!containerRoot) { + const appRoot = document.createElement("div"); + appRoot.id = containerName; + articleIframeDocument?.body?.insertAdjacentElement( + "beforeend", + appRoot + ); + containerRoot = + articleIframeDocument?.getElementById(containerName); + + if (containerRoot) { + containerRoot.style.width = "100%"; + setContainerRoot(containerRoot); + } + } + }, [articleIframeDocument]); + + if ( + !( + articleIframe && + articleIframeDocument && + articleIframe.contentDocument + ) + ) { + console.log(`iframe ${articleIframeName} not found`); + return null; + } + + return ( + containerRoot && ( + <> + {createPortal(, containerRoot)} + {/* Inject the styles into the iframe */} + {createPortal( + , + articleIframe.contentDocument.head + )} + + ) + ); +} diff --git a/packages/example/src/module/article/component/Read/index.tsx b/packages/example/src/module/article/component/Read/index.tsx index 2a65dbc22..18702e6c1 100644 --- a/packages/example/src/module/article/component/Read/index.tsx +++ b/packages/example/src/module/article/component/Read/index.tsx @@ -1,4 +1,5 @@ import { InjectBannerComponent } from "@/module/article/component/Read/InjectBannerComponent"; +import { InjectPopupComponent } from "@/module/article/component/Read/InjectPopupComponent"; import { InjectUnlockComponent } from "@/module/article/component/Read/InjectUnlockComponent"; import { Skeleton } from "@/module/common/component/Skeleton"; import type { Article } from "@/type/Article"; @@ -26,7 +27,7 @@ export function ReadArticle({ const [iframeRef, setIframeRef] = useState(null); // The nexus referral - useNexusReferral({ + const { data: referral } = useNexusReferral({ contentId: article.contentId as Hex, }); @@ -77,6 +78,9 @@ export function ReadArticle({ walletStatus?.key === "not-connected" && ( )} + {injecting > 0 && referral?.key === "referred-history" && ( + + )} {articleUnlockStatus && articleUnlockStatus?.key !== "waiting-response" ? (