Skip to content

Commit

Permalink
Merge pull request #272 from arconnectio/feat/arc-281-subscription-api
Browse files Browse the repository at this point in the history
feat: subscriptions
  • Loading branch information
nicholaswma authored May 28, 2024
2 parents 512983e + 0931c06 commit 126f1fb
Show file tree
Hide file tree
Showing 33 changed files with 5,046 additions and 2,760 deletions.
32 changes: 32 additions & 0 deletions assets/_locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -1765,5 +1765,37 @@
"token_imported": {
"message": "Token Succesfully Added",
"description": "token added"
},
"subscription_allowance": {
"message": "Subscription Allowance",
"description": "subscription settings title"
},
"subscription_title": {
"message": "Subscription",
"description": "subscription title"
},
"subscribe_description": {
"message": "APPNAME requests to set up a subscription with recurring payments",
"description": "subscribe description"
},
"subscription_description": {
"message": "Manage auto withdrawal allowance",
"description": "subscription setting description"
},
"subscription_cancelled": {
"message": "Subscription Cancelled",
"description": "The subscription has been successfully cancelled."
},
"subscription_cancelled_error": {
"message": "Subscription Cancellation Error",
"description": "There was an error cancelling the subscription. Please try again."
},
"subscription_deleted": {
"message": "Subscription Deleted",
"description": "The subscription has been successfully deleted."
},
"subscription_delete_error": {
"message": "Subscription Deletion Error",
"description": "There was an error deleting the subscription. Please try again."
}
}
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@
"@plasmohq/storage": "^1.7.2",
"@segment/analytics-next": "^1.53.2",
"@untitled-ui/icons-react": "^0.1.1",
"ar-gql": "1.2.9",
"ao-tokens": "^0.0.3",
"ar-gql": "^0.0.6",
"arbundles": "^0.9.5",
"arweave": "^1.13.0",
"axios": "^1.7.2",
Expand Down Expand Up @@ -108,7 +108,7 @@
"https-browserify": "^1.0.0",
"husky": "^8.0.0",
"path-browserify": "^1.0.1",
"plasmo": "^0.82.0",
"plasmo": "0.86.3",
"prettier": "^2.2.1",
"querystring-es3": "^0.2.1",
"semantic-release": "^23.0.0",
Expand All @@ -121,9 +121,9 @@
},
"resolutions": {
"arbundles/arweave": "^1.13.0",
"**/msgpackr": ">=1.10.1",
"human-crypto-keys/node-forge": "^1.3.1",
"human-crypto-keys/crypto-key-composer/node-forge": "^1.3.1",
"@parcel/runtime-js": "2.8.3",
"axios": "^1.7.2"
}
}
5 changes: 4 additions & 1 deletion src/api/background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ import verifyMessageModule from "./modules/verify_message";
import verifyMessage from "./modules/verify_message/verify_message.background";
import signDataItemModule from "./modules/sign_data_item";
import signDataItem from "./modules/sign_data_item/sign_data_item.background";
import subscriptionModule from "./modules/subscription";
import subscription from "./modules/subscription/subscription.background";

/** Background modules */
const modules: BackgroundModule<any>[] = [
Expand All @@ -60,7 +62,8 @@ const modules: BackgroundModule<any>[] = [
{ ...signMessageModule, function: signMessage },
{ ...privateHashModule, function: privateHash },
{ ...verifyMessageModule, function: verifyMessage },
{ ...signDataItemModule, function: signDataItem }
{ ...signDataItemModule, function: signDataItem },
{ ...subscriptionModule, function: subscription }
];

export default modules;
Expand Down
5 changes: 4 additions & 1 deletion src/api/foreground.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ import signMessageModule from "./modules/sign_message";
import signMessage, {
finalizer as signMessageFinalizer
} from "./modules/sign_message/sign_message.foreground";
import subscriptionModule from "./modules/subscription";
import subscription from "./modules/subscription/subscription.foreground";
import privateHashModule from "./modules/private_hash";
import privateHash, {
finalizer as privateHashFinalizer
Expand Down Expand Up @@ -90,7 +92,8 @@ const modules: ForegroundModule[] = [
...signDataItemModule,
function: signDataItem,
finalizer: signDataItemFinalizer
}
},
{ ...subscriptionModule, function: subscription }
];

export default modules;
Expand Down
1 change: 1 addition & 0 deletions src/api/modules/connect/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export type AuthType =
| "unlock"
| "token"
| "sign"
| "subscription"
| "signMessage"
| "signature"
| "signDataItem";
Expand Down
11 changes: 11 additions & 0 deletions src/api/modules/subscription/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { PermissionType } from "~applications/permissions";
import type { ModuleProperties } from "~api/module";

const permissions: PermissionType[] = ["ACCESS_ADDRESS"];

const subscriptionModule: ModuleProperties = {
functionName: "subscription",
permissions
};

export default subscriptionModule;
68 changes: 68 additions & 0 deletions src/api/modules/subscription/subscription.background.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import {
isAddress,
isLocalWallet,
isSubscriptionType
} from "~utils/assertions";
import { getActiveAddress, getActiveKeyfile, getWallets } from "~wallets";
import type { ModuleFunction } from "~api/background";
import authenticate from "../connect/auth";
import { getSubscriptionData } from "~subscriptions";
import {
RecurringPaymentFrequency,
type SubscriptionData
} from "~subscriptions/subscription";

const background: ModuleFunction<void> = async (
appData,
subscriptionData: SubscriptionData
) => {
// validate input
isAddress(subscriptionData.arweaveAccountAddress);

isSubscriptionType(subscriptionData);
const address = await getActiveAddress();

// if is hardware wallet
const decryptedWallet = await getActiveKeyfile();
isLocalWallet(decryptedWallet);

// check if subsciption exists
let subscriptions = await getSubscriptionData(address);

if (
subscriptions &&
subscriptions.find(
(subscription) =>
subscription.arweaveAccountAddress ===
subscriptionData.arweaveAccountAddress
)
) {
throw new Error("Account is already subscribed");
}

await authenticate({
type: "subscription",
url: appData.appURL,
arweaveAccountAddress: subscriptionData.arweaveAccountAddress,
applicationName: subscriptionData.applicationName,
subscriptionName: subscriptionData.subscriptionName,
subscriptionManagementUrl: subscriptionData.subscriptionManagementUrl,
subscriptionFeeAmount: subscriptionData.subscriptionFeeAmount,
recurringPaymentFrequency: subscriptionData.recurringPaymentFrequency,
nextPaymentDue: subscriptionData.nextPaymentDue,
subscriptionStartDate: subscriptionData.subscriptionStartDate,
subscriptionEndDate: subscriptionData.subscriptionEndDate,
applicationIcon: subscriptionData?.applicationIcon
});

subscriptions = await getSubscriptionData(address);
const subscription = subscriptions.find(
(subscription) =>
subscription.arweaveAccountAddress ===
subscriptionData.arweaveAccountAddress
);

return subscription;
};

export default background;
39 changes: 39 additions & 0 deletions src/api/modules/subscription/subscription.foreground.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import type { SubscriptionData } from "~subscriptions/subscription";
import type { ModuleFunction } from "~api/module";

const foreground: ModuleFunction<SubscriptionData[]> = (data) => {
// Validate required fields
const requiredFields: (keyof SubscriptionData)[] = [
"arweaveAccountAddress",
"applicationName",
"subscriptionName",
"subscriptionFeeAmount",
"recurringPaymentFrequency",
"subscriptionManagementUrl",
"subscriptionEndDate"
];

for (const field of requiredFields) {
if (data[field] === undefined) {
throw new Error(`Missing required field: ${field}`);
}
}

// optional fields
const allFields = [...requiredFields, "applicationIcon"];

Object.keys(data).forEach((key) => {
if (!allFields.includes(key as keyof SubscriptionData)) {
throw new Error(`Unexpected extra field: ${key}`);
}
});

return [
{
...data,
applicationIcon: data.applicationIcon
}
];
};

export default foreground;
8 changes: 6 additions & 2 deletions src/background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { onInstalled } from "~utils/runtime";
import browser from "webextension-polyfill";
import { syncLabels } from "~wallets";
import { trackBalance } from "~utils/analytics";
import { subscriptionsHandler } from "~subscriptions/api";

// watch for API calls
onMessage("api_call", handleApiCalls);
Expand All @@ -26,11 +27,14 @@ browser.tabs.onUpdated.addListener((tabId) => handleTabUpdate(tabId));
browser.tabs.onActivated.addListener(({ tabId }) => handleTabUpdate(tabId));

// handle fee alarm (send fees asyncronously)
browser.alarms.onAlarm.addListener(handleFeeAlarm);
// browser.alarms.onAlarm.addListener(handleFeeAlarm);

// handle norifications
// handle notifications
browser.alarms.onAlarm.addListener(notificationsHandler);

// handle subscriptions
browser.alarms.onAlarm.addListener(subscriptionsHandler);

browser.alarms.onAlarm.addListener(trackBalance);

// handle alarm for updating gateways
Expand Down
12 changes: 8 additions & 4 deletions src/components/dashboard/list/BaseElement.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,12 +73,16 @@ const ContentWrapper = styled.div`
gap: ${setting_element_padding};
`;

const SettingIconWrapper = styled(Squircle)`
export const SettingIconWrapper = styled(Squircle)<{
bg?: string;
customSize?: string;
}>`
position: relative;
flex-shrink: 0;
width: 2.6rem;
height: 2.6rem;
color: rgb(${(props) => props.theme.theme});
width: ${(props) => props.customSize || "2.6rem"};
height: ${(props) => props.customSize || "2.6rem"};
color: rgb(${(props) => props.bg || props.theme.theme});
`;

export const SettingIcon = styled(SettingsIcon)`
Expand Down
9 changes: 9 additions & 0 deletions src/components/popup/WalletHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,10 @@ import browser from "webextension-polyfill";
import styled from "styled-components";
import copy from "copy-to-clipboard";
import { type Gateway } from "~gateways/gateway";

import {
Bell03,
CreditCard01,
Container,
DotsVertical,
Expand01,
Expand Down Expand Up @@ -436,6 +438,13 @@ export default function WalletHeader() {
window.location.href.split("#")[0] + "?expanded=true"
);
}
},
{
icon: <CreditCard01 style={{ width: "18px", height: "17px" }} />,
title: "Subscriptions",
route: () => {
push("/subscriptions");
}
}
]}
/>
Expand Down
2 changes: 1 addition & 1 deletion src/components/popup/asset/Thumbnail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export default function Thumbnail({ src }: Props) {
<FullScreenButton
href={src}
target="_blank"
rel="noopener noreferer"
rel="noopener noreferrer"
onClick={(e) => e.stopPropagation()}
>
<MaximizeIcon />
Expand Down
2 changes: 1 addition & 1 deletion src/components/popup/home/AppIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export const NoAppIcon = styled(GlobeIcon)`
color: #fff;
`;

const AppIcon = styled(Squircle)<{ color?: string }>`
export const AppIcon = styled(Squircle)<{ color?: string }>`
color: ${(props) =>
props.color ? props.color : "rgb(" + props.theme.theme + ")"};
width: 3rem;
Expand Down
20 changes: 20 additions & 0 deletions src/popup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import SendAuth from "~routes/popup/send/auth";
import Explore from "~routes/popup/explore";
import Unlock from "~routes/popup/unlock";
import Notifications from "~routes/popup/notifications";
import Subscriptions from "~routes/popup/subscriptions/subscriptions";
import Tokens from "~routes/popup/tokens";
import Asset from "~routes/popup/token/[id]";
import Collectibles from "~routes/popup/collectibles";
Expand All @@ -29,6 +30,9 @@ import Recipient from "~routes/popup/send/recipient";
import Confirm from "~routes/popup/send/confirm";
import { NavigationBar } from "~components/popup/Navigation";
import MessageNotification from "~routes/popup/notification/[id]";
import SubscriptionDetails from "~routes/popup/subscriptions/subscriptionDetails";
import SubscriptionPayment from "~routes/popup/subscriptions/subscriptionPayment";
import SubscriptionManagement from "~routes/popup/subscriptions/subscriptionManagement";

export default function Popup() {
const theme = useTheme();
Expand Down Expand Up @@ -75,6 +79,22 @@ export default function Popup() {
</Route>
<Route path="/explore" component={Explore} />
<Route path="/unlock" component={Unlock} />
<Route path="/subscriptions" component={Subscriptions} />
<Route path="/subscriptions/:id">
{(params: { id: string }) => (
<SubscriptionDetails id={params?.id} />
)}
</Route>
<Route path="/subscriptions/:id/manage">
{(params: { id: string }) => (
<SubscriptionManagement id={params?.id} />
)}
</Route>
<Route path="/subscriptions/:id/payment">
{(params: { id: string }) => (
<SubscriptionPayment id={params?.id} />
)}
</Route>
<Route path="/notifications" component={Notifications} />
<Route path="/notification/:id">
{(params: { id: string }) => (
Expand Down
Loading

0 comments on commit 126f1fb

Please sign in to comment.