Coder Social home page Coder Social logo

interledger / web-monetization-extension Goto Github PK

View Code? Open in Web Editor NEW
47.0 5.0 2.0 1.45 MB

An open-source browser extension that enables Web Monetization.

License: Apache License 2.0

TypeScript 98.86% HTML 0.32% CSS 0.61% JavaScript 0.20%
hacktoberfest ilp interledger open-payments web-monetization

web-monetization-extension's People

Contributors

dependabot[bot] avatar devcer avatar dianafulga avatar ionutanin avatar raducristianpopa avatar renovate[bot] avatar sublimator avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

web-monetization-extension's Issues

Add base layout component

Context

Todos

  • Implement a base layout component that can be used in all views
  • It should contain:
    • layout header
    • main content
  • The layout header needs to have a switch component that enables/disables WM (global)

Note

Can be paired with #56.

One-time payment

Context

Todos

TBD

Considerations

Consider implementing this after streaming/continuous payments.
One-time payments should also be easier to implement once the continuous payment flow is complete.

Discussion point
Q: To discuss with Alex - does the one-time payment draw from the (monthly) budget already available, or not?
A: @raducristianpopa - To enhance UX, performing a one-time payment will deduct funds directly from the available (monthly) budget. This approach eliminates the need for an additional user action - approving a new grant.

Decision
alpha release: split once-off payments equally between the wallet addresses found on the website

Setup & migrate existing code to Tailwind

Context

Todos

  • Setup Tailwind
    • Add current design colors in the Tailwind config (Figma file)
  • Migrate existing components to Tailwind (if some of the current ones can be reused/adapted)

Add addional unit tests in listener.test.ts

  • dispatches the custom ‘monetization’ event
  • dispatches the custom event at regular intervals
  • Multiple links scenario
  • the case when browser object is not defined(not to throw error)
  • Handling missing href in the link attribute

Settings page - Connect wallet

Context

Implement settings (setup) page.

Todos

  • For a user that does not have a connected wallet address, the settings page should be displayed
  • Add setup form:
    • wallet address
    • amount
    • amount type (recurring/single use)
  • Validate form fields on submission
  • For a user that has a connected wallet address, all the setup forms fields should be disabled and read-only. The only action that the user can perform: disconnecting the wallet address

Handle globally en/disabled WM

Context

  • #47
  • Users have the option to en/disable WM globally (which applies for every website)

Dependency

This is dependent on ticket #58

Todos

  • Disabled: display an informational message in the main page, that notifies the user about WM being turned off
  • Enabled: inject polyfill if enabled

Add button component

Context

Todos

  • Implement a reusable button component
  • The button should have two variants:
    • default
    • destructive
  • The component should have handling for hover, focus, and other required states
  • Consider following the WAI ARIA Button pattern for a11y guidelines

Note

For handling different variants CVA can be used.

Polyfil Injection and CSP

  • inject document monetization in content script - old polyfill from coil
  • configure CSP hash to allow injection of polyfill

Add extension storage wrapper

Context

Create a storage wrapper that has the ability to return values (single or multiple) in a type-safe manner.

Todos

TBD

Related tickets: #92 (please see PR review comments for the #93 ). The intention is to (possibly) move some of the code that to this wrapper, which was originally submitted in the PR.

Configure popup store

Context

Todos

  • Information stored in the extension storage should be sent to the popup
  • Setup a context provider that registers of all the information that was sent from the extension
  • The amount that needs to be sent per second (derived from the rate of pay value that is received from the extension storage)

Info: related to task #58

Add IoC container

Context

  • Add IoC container for dependency injection
  • Purpose: register all background service worker modules into a single container

Todos

  • Research IoC libraries - they need to work in a browser environment
    • Known libraries: Awilix, Inversify, injection-js

Configure extension storage

Context

Todos

  • WM enabled flag (global)
  • List of websites where WM was disabled by the user
  • Wallet address information (id, authentication server, resource server, asset code, asset scale)
  • Remaining balance (can be stored alongside wallet address information?)
  • Amount type (recurring or single-use)
    • can be a flag (recurring: true | false)
  • Access tokens & manage URLs for:
    • quotes
    • outgoing payments
  • Rate of pay (cents per hour)

Info: related to task #59

Display WM icon differently if website is/isn't monetized

Context

When a user has the WM extension installed, and they have successfully connected their wallet address to the extension, then the icon of the extension must look different depending on whether the user is currently visiting a web monetized website or not.

When a user is visiting a site:

  • If the site has no monetization tag with a valid wallet address url:
  • If the site has at least one monetization tag with a valid wallet address url:
  • If extension has web monetization globally disabled:

Move to MV3 as MV2 is going to be deprecated

CORS on MV3
Use MonetizationEvent class rather than using custom events
Add script injection into content script on MV3 with monetization event (if necessary add script CSP sha)

  • The component that searches for WM tags and event routing to WM tags should not run in the main JS context

References on Chrome extension MV2 sunset timeline:

Trigger `load` event after checking the wallet address

At the moment, the load event is triggered automatically when the WM polyfill is injected which is not the intended behaviour. The monetization link should be checked before triggering the load event - spec.

  • Inject polyfill on document_start (before window.onload) (--- from #78 , implemented here)

Rate of pay slider

Context

Todos

  • Add rate of pay slider
  • The slider should have these hourly rates:
    • default: 0.60 (i.e. sixty cents per hour)
    • step: 0.01
    • min: 0.00
    • max: 1.20 (not sure about this value at the moment, but it can be easily changed)
  • Whenever the user changes the slider value, update the rate of pay in the extension storage and recalculate the amount that needs to be sent every second (which will be stored in the extension state)
  • Display the current rate of pay and the remaining balance
  • Add switch component to disable WM for the current website

Note

If the slider is dragged to its minimum value (0.00), we can disable the WM for the current website.

Linked to #53

[Monetization flow]

Content Script

  1. Setup MutationObserver to detect link tags
  2. For each found tag check if the href has a valid JSON response (based on OP Spec)
  3. For each valid tag save details (Wallet Address response - authServer, resourceServer etc ... + a unique identifier - uuidv4, w/e)
    in a map/array/something and send START_MONETIZATION (payload wallet JSON - opt requestId TBD) event to background script
  4. Receicer for MONETIZATION_EVENT that will dispatch a MonetizationEvent to the link tag
  5. Watch for document visibility change
    • on hidden, stop monetization
      • send "STOP_MONETIZATION" event with payload requestId
    • on visible, resume monetization
      • send "RESUME_MONETIZATION" event with payload requestId

Background
4. In background - call monetizationServer.start()
4.1 request incoming payment grant
4.2 create incoming payment - NO AMOUNT
4.3 try to send payment - if received status code is 403, rotate token
4.4 revoke grant after creating incoming payment
4.5 add incoming payment details to a map - (key: tabId, value: {"uuidv4" : { ...incomingPaymentDetails, active: true }, ...})
5. Create quote with receiveAmount = RATE_OF_PAY / 3600 to incoming payment (active: true)
6. Create outgoing payment with quote identifier (active: true)
7. Send "MONETIZATION_EVENT" to content script with MonetizationEvent payload + request id
8. Watch for document visibility change

  • on hidden, stop monetization
    • receive "STOP_MONETIZATION" event with payload requestId and mark active false for that requestId
  • on visible, resume monetization
    • receive "RESUME_MONETIZATION" event with payload requestId and mark active true for that requestId
    • repeat steps 5-6 - if IP expired, go back to 4.1 and update payment details instead of adding a new record
  1. Add tab listener to watch for closed tabs and remove payment details from map (step 4.5) for that specific tab

TODO:

Move OP usage to a server

Context

For security reason, the OP usage is gonna be moved to a server.

Connecting a wallet

sequenceDiagram
    autonumber
    %% Extension ->> Extension: When the extension is installed generate an <br> opaque token
    %% Extension ->>+ Proxy: Send opaque token
    %% Proxy ->> Proxy: Store the opaque token in <br> Redis/memory/SQLite ?
    %% Proxy ->>- Extension: Response (200/400)
    %% Note over Extension, Proxy: On every request, the extension is going <br> to include the opaque token in a header

    Extension ->> Extension: User fills in the Connect your wallet form <br> and submits it
    Extension ->>+ Proxy: Send form data (wallet address, <br> amount, amount type)

    %% Proxy ->> Proxy: Verifies if the opaque token exists <br> and increases it's usage by 1
    %% Note right of Proxy: The opaque token will be used for rate limiting. <br> 100req/min ?

    Proxy ->> ASE Resource: Fetch wallet address information
    ASE Resource ->> Proxy: Return wallet address information <br> or throws an error

    break if the wallet address does not exist
        Proxy ->> Extension: Return an error
    end

    Proxy ->> ASE Auth: Request a grant with access to quotes <br/> and outgoing payments
    ASE Auth ->> Proxy: Return interactive grant details
    Proxy ->>- Extension: Return interactive grant information <br> (interaction URL, continue URI and continue token) <br> continue URI and token needs to be stored <br> in extension until the user interacts with the grant
    Extension ->> Browser: Open a new tab and navigate to the interaction URL
    Extension ->> Extension: Listen for tab changes
    Browser ->> Browser: User interacts with the grant <br> (accept/decline)
    Browser ->> Browser: User gets redirect to the finish URL
    Extension ->> Extension: Check if the finish URL has an interaction <br> reference

    break if the user declined the grant - no interaction reference
        Extension ->> Extension: Clear extension state & storage (delete <br> continue URI, token)
        Note over Extension: The interaction "response" can be checked <br> by verifying the "result" query <br> parameter as well: <br> - "grant_rejected": user declined <br> - "grant_invalid": not in a state where it may be <br> accepted or rejected
    end

    Extension ->> Proxy: Send the interaction reference alongside with <br> the continuation URI and token
    Proxy ->> ASE Auth: Make continuation request
    ASE Auth ->> Proxy: Return access token
    Note over Proxy, ASE Auth: This access token has access to quotes <br> and outgoing payments. There is no <br> need to have two access tokens <br> (one for quotes and one for outgoing <br> payments).
    Proxy ->> Proxy: Generate a signed token based on the user wallet address
    Note over Proxy: Look into ways to sign a token (JWT, Iron, <br> PASETO)
    Proxy ->> Extension: Return access token(s) + manage URL(s) <br> + signed token
    Extension ->> Extension: Store access token + manage URL and <br> the signed token in extension storage
    Note over Extension: User wallet address <br> is now connected
Loading

Monetization Flow

sequenceDiagram
    autonumber
    WM Provider ->> WM Provider: Check if WM is enabled globally
    break if WM is disabled
        WM Provider-->WM Provider: STOP
    end
    WM Provider ->> WM Provider: Check if WM is enabled for the <br> current website
    break if WM is disabled
        WM Provider-->WM Provider: STOP
    end
    WM Provider ->> WM Page: Grabs website wallet address
    WM Provider ->>+ ASE1 Resource: Request wallet address information
    ASE1 Resource -->>- WM Provider: Returns wallet address information
    WM Provider ->> WM Provider: If the JSON response passes validation <br> (matches OpenAPI Spec) fire the `load` <br> event on the link element
    WM Provider ->>+ Proxy: Send the wallet address URL
    Proxy ->> ASE1 Auth: Requests an incoming payment grant <br> with the "create" access
    ASE1 Auth ->> Proxy: Returns access token
    Proxy ->> ASE1 Resource: Create an incoming payment <br> with no amount
    ASE1 Resource ->> Proxy: Returns incoming payment information
    Proxy ->> ASE1 Auth: Revokes token
    Note over Proxy, ASE1 Auth: Should the token be stored for later usage?
    Proxy -->>- WM Provider: Returns incoming payment URL
    WM Provider ->> WM Provider: Stores the incoming payment URL until the vistor <br> changes the tab or navigates to another website
    Note left of WM Provider: We need to perform the flow from step 1 if: <br> - a navigation event is happening <br> - the wallet address that was found <br> in the page changed <br> - when the IP expires
    break if WM is disabled
        WM Provider-->WM Provider: STOP
    end

    WM Provider ->> WM Provider: Calculate the amount that needs to be sent <br>  every second (based on the rate of pay) <br> and save it in the extension state for later use

    loop Every second
        WM Provider ->>+ Proxy: Send incoming payment URL, amount, access token + include signed token in the header <br> The manage URLs should be sent as well to rotate the tokens if they are expired.
        Note over WM Provider, Proxy: We should not have a retry mechanism for failed requests. Even if <br> requests are failing we need to keep this loop going. 
        Proxy ->> ASE2 Resource: Create quote (with incoming payment URL and amount)
        break if the incoming payment is expired we need to perform the whole flow again
            WM Proxy -->> Proxy: STOP
        end
        critical if the access token is expired, rotate it and return the new token and manage URL
            Proxy -->> ASE2 Auth: Rotate access token
            ASE2 Auth -->> Proxy: Return new access token and manage URL
        end
        Note over Proxy: If the token got rotated, we will need to use the <br> new access token for the upcoming requests
        ASE2 Resource ->> Proxy: Returns quote information
        Proxy ->> ASE2 Resource: Create outgoing payment
        ASE2 Resource ->> Proxy: ASE Resource Server response (success/failure)
        Proxy ->>- WM Provider: Forward ASE Resource Server response
        Note over WM Provider, Proxy: Since the access token manage URL is passed as well, the backend <br> can return the new access token and the new manage URL <br> if the token gets rotated.
    end
Loading

Todos

Add Labeler action

Automatically applies labels to pull requests to provide additional context about the changes.

Add switch/toggle component

Context

Todos

  • Implement a reusable switch component
  • The switch should have two size variants:
    • default (used in the popup header to disable WM globally)
    • sm (used in the main page to disable continuous payments stream for a certain site)
  • The component should have handling for focus, disabled and other required states
  • Consider following the WAI ARIA Switch pattern for a11y guidelines

Note

For handling different variants CVA can be used.

CustomEvent properties inside `detail` won't match spec

Issue: Non-Standard Behavior of CustomEvent in Web Monetization Polyfill

Problem Description

When using the CustomEvent object in the Web Monetization Polyfill, all properties are bundled inside event.detail. This behavior does not align with the Web Monetization specification, which expects these properties to be directly accessible on the event object.

Suggested Solution: Using Event Subclass

To conform to the spec, an Event subclass can be created. However, this subclass must be created in the main page JS context.


MV2 Implementation Details: Limitations and Workarounds

In MV2, directly injecting a <script> tag with src=... into the main document will not work reliably. The script will load asynchronously and may not execute before it's actually needed or used.

The workaround is to inject the script content directly, ensuring synchronous loading and execution. Here's a simplified version of the tested code for this:

function injectCode(code: string) {
  inject(script => (script.innerHTML = code));
}

function inject(configure: (script: HTMLScriptElement) => void) {
  const script = document.createElement('script');
  configure(script);
  document.documentElement.appendChild(script);
  document.documentElement.removeChild(script); // Clean up
}

Handling CSP in MV2

To handle CSP issues, you can include a content_security_policy in the manifest.json:

"content_security_policy": "script-src 'self' 'sha256-POLYFILL-HASH='; object-src 'self'"

In your build script:

const data = Buffer.from(polyfill.content, 'utf-8');
const digest = createHash('sha256').update(data).digest();
const polyfillHash = `sha256-${digest.toString('base64')}`;

v2.content_security_policy = v2.content_security_policy.replace(
  'sha256-POLYFILL-HASH=',
  polyfillHash
);

MV3 Implementation Details

In MV3, the scripting API from Chrome can be used to register a content script that runs in the main page JS context. Below is a sample TypeScript code snippet:

chrome.scripting.registerContentScripts([
  {
    id: 'wm-polyfill.js',  // Unique identifier for the content script
    matches: ['https://*/*', 'http://*/*'],  // URL patterns where the script will run
    js: ['wm-polyfill.js'],  // JavaScript file to be injected
    allFrames: true,  // Run the script in all frames
    world: 'MAIN',  // Execute in the main world
    runAt: 'document_start'  // Timing for script injection
  }
])

Note: You may need to deal with Duplicate script ID issues due to the service worker life cycles. Handling this is an exercise left to the reader.

Points to Note

  • For MV2, use inline script content to avoid race conditions and ensure synchronous execution.
  • For MV3, setting the world property to MAIN is essential for the solution to work.

Build on GitHub

  • Manual workflow dispatch for releasing a new version
  • Build extension for all browsers for every PR and delete all uploaded artifacts when the PR is closed

Add label component

Context

Todos

  • Implement a label component, that can be used for multiple form items
  • Prevent text selection on double click

Add popup routing mechanism

Context

Todos

  • react-router-dom's createMemoryRouter can be used for routing
  • Considerations:
    • other routing mechanisms (wouter or other libraries)?
    • using a simple React Context that can keep track of the current active page?

Note

Can be paired with #55.

Add input component

Context

Todos

  • Implement a reusable input component (text)
  • For the amount input, we can use react-number-format
  • The component should have handling for focus, disabled and other required states

Note

We can have two separate input components (text and amount).
A separate label component can be added, which can be used for other form items as well.

Add local HTTP server to generate signatures

Context

The private key and key id should not be bundled with the extension

Todos

  • For local development, setup a HTTP server that generates the headers and returns them to the extension
  • Add Webpack define plugin for development and production builds
    • the local server should be used in development
    • an external function should be used when packing the extension

Backend scaffold

Context

Todos

  • Remove current local signatures app
  • Scaffold new backend (Koa)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.