Skip to content

Commit dabdc28

Browse files
committed
Initial commit: UniRate Sheets add-on v0.1.0
Apps Script add-on exposing UniRate as Google Sheets custom functions: - =UNIRATE(from, to, [amount], [date]) — current/historical conversion - =UNIRATE_RATES(base) — all rates for a base currency - =UNIRATE_HISTORICAL(date, base) — Pro-gated historical rate table - =UNIRATE_TIMESERIES(start, end, base, currencies) — Pro timeseries - =UNIRATE_CURRENCIES() — supported currency codes - =UNIRATE_VAT([country]) — EU/UK VAT rates Sidebar for storing the API key in document properties; onOpen menu with Set/Test/Clear/Help; Workspace add-on Card for the side rail. urlFetchWhitelist locked to https://api.unirateapi.com/. Cached in CacheService for 1h (live rates) / 6h (historical, currency lists). 30 mock tests via jest with stubbed UrlFetchApp/PropertiesService/ CacheService. CI matrix on Node 18/20/22. Marketplace SDK and OAuth-verification copy lives under marketplace/.
0 parents  commit dabdc28

18 files changed

Lines changed: 8838 additions & 0 deletions

.clasp.json.example

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"scriptId": "REPLACE_WITH_YOUR_APPS_SCRIPT_ID",
3+
"rootDir": "src",
4+
"parentId": []
5+
}

.claspignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# clasp pushes from src/ (set "rootDir": "src" in .clasp.json), so this is
2+
# scoped to that subtree. Keep test fixtures/docs out of the deployable.
3+
**/node_modules/**
4+
**/tests/**
5+
**/*.test.js
6+
**/.git/**

.github/workflows/test.yml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
name: test
2+
on:
3+
push:
4+
branches: [main]
5+
pull_request:
6+
jobs:
7+
jest:
8+
runs-on: ubuntu-latest
9+
strategy:
10+
matrix:
11+
node: ['18', '20', '22']
12+
steps:
13+
- uses: actions/checkout@v4
14+
- uses: actions/setup-node@v4
15+
with:
16+
node-version: ${{ matrix.node }}
17+
cache: npm
18+
- run: npm ci
19+
- run: npm test

.gitignore

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
node_modules/
2+
coverage/
3+
.clasp.json
4+
.claspignore.local
5+
*.log
6+
.DS_Store
7+
.env
8+
.env.*
9+
!.env.example

CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Changelog
2+
3+
## v0.1.0 — 2026-05-10
4+
5+
Initial release.
6+
7+
- `=UNIRATE(from, to, [amount], [date])` — current or historical conversion.
8+
- `=UNIRATE_RATES(base)` — all current rates for a base currency, as a 2-column array.
9+
- `=UNIRATE_HISTORICAL(date, base)` — Pro-gated historical rate table for a date.
10+
- `=UNIRATE_TIMESERIES(start, end, base, currencies)` — Pro-gated timeseries.
11+
- `=UNIRATE_CURRENCIES()` — supported currency codes.
12+
- `=UNIRATE_VAT([country])` — EU/UK VAT rates.
13+
- Sidebar for storing the API key in document properties.
14+
- `Extensions → UniRate` menu with **Set API key**, **Test connection**, **Help**.
15+
- 6-hour `CacheService` cache on rates / currencies / VAT to keep recalc traffic well under free-tier limits.

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 Unirate Team
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
# UniRate for Google Sheets
2+
3+
`=UNIRATE("USD","EUR")` — live currency rates, historical lookups, and EU/UK
4+
VAT rates as Sheets formulas, backed by the [UniRate API](https://unirateapi.com).
5+
6+
> Get a free API key at [unirateapi.com/signup](https://unirateapi.com/signup) (1,000 requests/day, no card).
7+
8+
## Functions
9+
10+
| Formula | Returns |
11+
|---|---|
12+
| `=UNIRATE("USD","EUR")` | Live rate, 1 USD → ? EUR. |
13+
| `=UNIRATE("USD","EUR",100)` | 100 USD converted at the live rate. |
14+
| `=UNIRATE("USD","EUR",100,"2024-01-15")` | Historical conversion (Pro). |
15+
| `=UNIRATE_RATES("USD")` | All current rates against USD as a 2-column array. |
16+
| `=UNIRATE_HISTORICAL("2024-01-15","USD")` | All historical rates for a date (Pro). |
17+
| `=UNIRATE_TIMESERIES("2024-01-01","2024-01-31","USD","EUR,GBP")` | Date × currency rate matrix (Pro). |
18+
| `=UNIRATE_CURRENCIES()` | Single-column list of supported currency codes. |
19+
| `=UNIRATE_VAT()` / `=UNIRATE_VAT("DE")` | EU/UK VAT — full table or a single rate. |
20+
21+
The 4-arg historical form, `UNIRATE_HISTORICAL`, and `UNIRATE_TIMESERIES`
22+
require a UniRate Pro plan; without one they return a clear "Pro plan"
23+
message in the cell tooltip rather than a silent error.
24+
25+
Currency codes are case-insensitive and trimmed. Dates accept either a
26+
`YYYY-MM-DD` string or a Sheets date cell.
27+
28+
## Install (end users)
29+
30+
> A Workspace Marketplace listing is in OAuth verification. Until that
31+
> goes live, install via "Test as add-on" or by copying the script.
32+
33+
1. Open a Google Sheet.
34+
2. **Extensions → Apps Script**.
35+
3. Replace `Code.gs` with the contents of `src/Code.js`.
36+
4. Add a new file `Setup.gs` with the contents of `src/Setup.js`.
37+
5. Add a new HTML file `Sidebar` with the contents of `src/Sidebar.html`.
38+
6. Open `appsscript.json` (gear icon → "Show appsscript.json") and replace
39+
it with `src/appsscript.json`.
40+
7. Save, refresh the sheet, accept the OAuth prompt.
41+
8. **Extensions → UniRate → Set API key…** — paste your key and save.
42+
9. Try `=UNIRATE("USD","EUR")` in any cell.
43+
44+
## Develop
45+
46+
Local mock tests run under Node — no Apps Script account required.
47+
48+
```bash
49+
npm install
50+
npm test # 30 jest tests, no network
51+
```
52+
53+
The Apps Script source itself is pushed via [`clasp`](https://github.com/google/clasp):
54+
55+
```bash
56+
npx clasp login
57+
npx clasp create --type sheets --rootDir src --title "UniRate for Sheets"
58+
# clasp writes ./.clasp.json with your scriptId — keep it gitignored.
59+
npx clasp push
60+
```
61+
62+
`.claspignore` keeps tests, `node_modules`, and CI config out of the
63+
deployable. Only `src/` is shipped.
64+
65+
## Caching and quota
66+
67+
Each successful API call is cached in [`CacheService`](https://developers.google.com/apps-script/reference/cache/cache-service)
68+
for 1 hour (current rates) or 6 hours (historical and currency-list lookups).
69+
A sheet with hundreds of `=UNIRATE("USD","EUR")` formulas fans out to one
70+
HTTP call per hour, so a free-tier key handles realistic load.
71+
72+
## Marketplace listing — submission checklist
73+
74+
This add-on is set up to be published to the Google Workspace Marketplace.
75+
Steps left, all of which require clicks in the Google Cloud Console:
76+
77+
1. **GCP project** — create one at <https://console.cloud.google.com/projectcreate>.
78+
Pick a name like `unirate-sheets-addon`.
79+
2. **OAuth consent screen** — APIs & Services → OAuth consent screen.
80+
- User type: **External**.
81+
- App name: `UniRate for Sheets`.
82+
- Support email: `admin@unirateapi.com`.
83+
- App logo: upload `marketplace/icon-128.png`.
84+
- Developer contact: `admin@unirateapi.com`.
85+
- Authorized domains: `unirateapi.com`.
86+
- Scopes — add the three from `src/appsscript.json`:
87+
- `…/auth/script.external_request`
88+
- `…/auth/script.container.ui`
89+
- `…/auth/spreadsheets.currentonly`
90+
- Optional/test users → leave empty (we want public).
91+
3. **Switch the Apps Script project to this GCP project**
92+
Apps Script editor → Project Settings → "Change project" → enter the
93+
GCP project number from step 1.
94+
4. **Enable Marketplace SDK** — APIs & Services → Library → search for
95+
"Google Workspace Marketplace SDK" → Enable.
96+
5. **Configure the Marketplace listing** — IAM & Admin → Marketplace SDK →
97+
Configuration / App listing tabs:
98+
- Application icon: 128×128 PNG.
99+
- Banner: 220×140 PNG.
100+
- Screenshots: 1280×800 PNG (3–5 of them; placeholders in `marketplace/`).
101+
- Privacy policy: `https://unirateapi.com/privacy`.
102+
- Terms of service: `https://unirateapi.com/terms`.
103+
- Detailed description: copy from `marketplace/listing-description.md`.
104+
6. **Verify domain** — Search Console → add `unirateapi.com` if not already.
105+
7. **Submit for OAuth verification** — OAuth consent screen → Publish app →
106+
"Prepare for verification". Google asks for:
107+
- Justification per scope (we use only the three above; copy reasoning
108+
from `marketplace/oauth-justifications.md`).
109+
- A demo video of the install + key-entry + one formula evaluation,
110+
≤ 2 min, captioned.
111+
- Domain ownership confirmation (already done in step 6).
112+
8. **Submit for Marketplace publish** — Marketplace SDK → Publish.
113+
Listing review and OAuth verification run in parallel; both typically
114+
take 1–2 weeks.
115+
116+
While verification is pending the script can still be installed by hand
117+
(see "Install" above). The OAuth grant warns "unverified app" until
118+
Google approves; that goes away once verification clears.
119+
120+
## License
121+
122+
MIT — see [LICENSE](LICENSE). Copyright © 2026 Unirate Team.

marketplace/README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Marketplace assets
2+
3+
Drop the production icon + screenshots here before submitting to the
4+
Workspace Marketplace.
5+
6+
| File | Spec | Used for |
7+
|---|---|---|
8+
| `icon-128.png` | 128×128 PNG, transparent background | Add-on logo (manifest + listing). |
9+
| `banner-220x140.png` | 220×140 PNG | Marketplace listing banner. |
10+
| `screenshot-1.png` | 1280×800 PNG | Listing screenshot — `=UNIRATE("USD","EUR")` in a cell. |
11+
| `screenshot-2.png` | 1280×800 PNG | Listing screenshot — sidebar with API-key entry. |
12+
| `screenshot-3.png` | 1280×800 PNG | Listing screenshot — `=UNIRATE_TIMESERIES` rendering a chart. |
13+
14+
Listing copy lives in [`listing-description.md`](listing-description.md).
15+
OAuth verification justifications live in [`oauth-justifications.md`](oauth-justifications.md).

marketplace/listing-description.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Marketplace listing copy
2+
3+
## Short description (≤ 80 chars)
4+
Live currency rates and historical FX in any Sheet — `=UNIRATE("USD","EUR")`.
5+
6+
## Detailed description (Markdown allowed in the GCP listing form)
7+
8+
Add live currency exchange rates, historical FX data, and EU/UK VAT rates to any Google Sheet. Use it like a spreadsheet function:
9+
10+
- `=UNIRATE("USD","EUR")` — live rate.
11+
- `=UNIRATE("USD","EUR",100)` — convert 100 USD to EUR.
12+
- `=UNIRATE("USD","EUR",100,"2024-01-15")` — historical conversion.
13+
- `=UNIRATE_RATES("USD")` — all rates for a base currency.
14+
- `=UNIRATE_TIMESERIES("2024-01-01","2024-01-31","USD","EUR,GBP")` — date × currency matrix.
15+
- `=UNIRATE_CURRENCIES()` — list of 170+ supported codes.
16+
- `=UNIRATE_VAT("DE")` — current VAT rates for EU + UK.
17+
18+
**Why UniRate**
19+
20+
- Free tier: 1,000 requests/day with no credit card.
21+
- 170+ currencies including crypto, daily history back to 1999 (Pro).
22+
- Results cached for 1 h on rates, 6 h on historical lookups, so a sheet
23+
with hundreds of formulas evaluates to one HTTP call per hour.
24+
25+
**How it works**
26+
27+
1. Install the add-on.
28+
2. Open **Extensions → UniRate → Set API key…**.
29+
3. Paste your free key from [unirateapi.com/signup](https://unirateapi.com/signup).
30+
4. Use any of the formulas above in any cell.
31+
32+
The API key is stored in the spreadsheet's document properties only — no
33+
data leaves your sheet except the currency codes and dates UniRate needs
34+
to fulfil the request.
35+
36+
**Open source**
37+
38+
Code is MIT-licensed and hosted at <https://github.com/UniRate-API/unirate-sheets>.
39+
40+
**Support**
41+
42+
- Email: support@unirateapi.com
43+
- Bug reports: <https://github.com/UniRate-API/unirate-sheets/issues>
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# OAuth scope justifications (paste verbatim into the consent-screen form)
2+
3+
The add-on requests three scopes. Each is the **narrowest** scope that
4+
satisfies the listed user-facing function.
5+
6+
## `https://www.googleapis.com/auth/script.external_request`
7+
8+
Required to call `https://api.unirateapi.com/api/*` from `UrlFetchApp`.
9+
Without this scope no formula can return a rate; the entire add-on is
10+
non-functional. Outbound traffic is restricted in `appsscript.json` via
11+
`urlFetchWhitelist` to `https://api.unirateapi.com/` only — Apps Script
12+
enforces this allowlist and blocks calls to any other host.
13+
14+
User-visible functions that depend on this scope: every `UNIRATE_*` custom
15+
function, the **Test connection** menu item, and the sidebar **Save & test**
16+
button.
17+
18+
## `https://www.googleapis.com/auth/script.container.ui`
19+
20+
Required to render the **UniRate API key** sidebar (`HtmlService` modal /
21+
sidebar surfaces) and to display alert dialogs from
22+
**Extensions → UniRate → Clear API key**. No spreadsheet content is read
23+
or modified through this scope.
24+
25+
## `https://www.googleapis.com/auth/spreadsheets.currentonly`
26+
27+
Limited per-document scope (rather than the broad `…/auth/spreadsheets`)
28+
needed by `SpreadsheetApp.getUi()` and `SpreadsheetApp.getActive().toast(…)`
29+
when surfacing the menu, the alert dialogs, and the "UniRate connected"
30+
toast notification. The scope only grants access to the spreadsheet the
31+
add-on is currently open in.
32+
33+
## What is *not* requested
34+
35+
The add-on intentionally avoids:
36+
37+
- `…/auth/spreadsheets` (full Sheets) — the `*.currentonly` variant covers
38+
every UI surface we use.
39+
- `…/auth/userinfo.email` / `…/auth/userinfo.profile` — we never need to
40+
identify the user; the API key is keyed to the document, not the person.
41+
- `…/auth/drive*` — we never list, open, or modify Drive files outside
42+
the active spreadsheet.
43+
44+
## Demo video script (≤ 2 min)
45+
46+
1. Cold-load a fresh Google Sheet.
47+
2. Install the add-on from the Marketplace listing.
48+
3. Approve the OAuth prompt — voiceover names each scope as the consent
49+
screen renders it.
50+
4. **Extensions → UniRate → Set API key…** — paste a free-tier key, click
51+
**Save & test**, sidebar shows "Connected. 170 currencies available."
52+
5. In cell A1 type `=UNIRATE("USD","EUR")` — value renders.
53+
6. In cell A2 type `=UNIRATE("USD","EUR",100,"2024-01-02")` — historical
54+
conversion renders (or, on a free key, a clear "Pro plan required"
55+
tooltip — show both flows if time permits).
56+
7. Stop recording.

0 commit comments

Comments
 (0)