Skip to content

Commit 586a596

Browse files
committed
feat: add export-user-data demo app
1 parent 30ad4ea commit 586a596

File tree

10 files changed

+274
-0
lines changed

10 files changed

+274
-0
lines changed
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Logs
2+
logs
3+
*.log
4+
npm-debug.log*
5+
yarn-debug.log*
6+
yarn-error.log*
7+
pnpm-debug.log*
8+
lerna-debug.log*
9+
10+
node_modules
11+
dist
12+
dist-ssr
13+
*.local
14+
15+
# Editor directories and files
16+
.vscode/*
17+
!.vscode/extensions.json
18+
.idea
19+
.DS_Store
20+
*.suo
21+
*.ntvs*
22+
*.njsproj
23+
*.sln
24+
*.sw?
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7+
<title>Vite + React + TS</title>
8+
</head>
9+
<body>
10+
<div id="root"></div>
11+
<script type="module" src="/src/main.tsx"></script>
12+
</body>
13+
</html>
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"name": "demo",
3+
"private": true,
4+
"version": "0.0.0",
5+
"type": "module",
6+
"scripts": {
7+
"dev": "vite",
8+
"build": "tsc && vite build",
9+
"preview": "vite preview"
10+
},
11+
"dependencies": {
12+
"firebase": "^9.11.0",
13+
"react": "^18.2.0",
14+
"react-dom": "^18.2.0"
15+
},
16+
"devDependencies": {
17+
"@types/react": "^18.0.17",
18+
"@types/react-dom": "^18.0.6",
19+
"@vitejs/plugin-react": "^2.1.0",
20+
"typescript": "^4.6.4",
21+
"vite": "^3.1.0"
22+
}
23+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { initializeApp } from "firebase/app";
2+
import { getAuth } from "firebase/auth";
3+
import { getFirestore } from "firebase/firestore";
4+
import { getStorage } from "firebase/storage";
5+
import { getFunctions } from "firebase/functions";
6+
7+
const firebaseConfig = {
8+
apiKey: "AIzaSyA7qjG8LNbBPeHi1gJemos-Ryf8ph4M2vk",
9+
authDomain: "export-user-data-extension.firebaseapp.com",
10+
databaseURL: "https://export-user-data-extension-default-rtdb.firebaseio.com",
11+
projectId: "export-user-data-extension",
12+
storageBucket: "export-user-data-extension.appspot.com",
13+
messagingSenderId: "75119052956",
14+
appId: "1:75119052956:web:6ac6840dd4331536914559",
15+
};
16+
export const app = initializeApp(firebaseConfig);
17+
export const auth = getAuth(app);
18+
export const firestore = getFirestore(app);
19+
export const storage = getStorage(app);
20+
export const functions = getFunctions(app, "us-central1");
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import React, { useState } from 'react'
2+
import ReactDOM from 'react-dom/client'
3+
import { auth, firestore, functions, storage } from './firebase'
4+
import { signInAnonymously } from 'firebase/auth'
5+
import { httpsCallable } from 'firebase/functions'
6+
import { doc, onSnapshot } from 'firebase/firestore'
7+
import { listAll, ref, getDownloadURL, getBlob } from 'firebase/storage'
8+
9+
import styles from './styles.module.css'
10+
11+
// Ensure the user is signed in before showing the app.
12+
signInAnonymously(auth).then(() => {
13+
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
14+
<React.StrictMode>
15+
<App />
16+
</React.StrictMode>
17+
);
18+
})
19+
20+
function App() {
21+
const [exporting, setExporting] = useState(false);
22+
const [files, setFiles] = useState<string[]>();
23+
24+
// Kicks off a new export task.
25+
async function onExport({ zip }: { zip: boolean }) {
26+
setExporting(true);
27+
28+
// Trigger the export.
29+
const result = await httpsCallable<void, { exportId: string }>(functions, `ext-export-user-data-${zip ? '' : 'rapy-'}exportUserData`)();
30+
// Get the returned export id.
31+
const exportId = result.data.exportId;
32+
33+
// Make a Firestore reference to the export.
34+
const documentRef = doc(firestore, 'exports', exportId);
35+
36+
// Listen for changes to the export - when complete returned the storage path of the export items.
37+
const { storagePath, zipPath } = await new Promise<{
38+
storagePath: string;
39+
zipPath?: string;
40+
}>((resolve, reject) => {
41+
const unsubscribe = onSnapshot(documentRef, (snapshot) => {
42+
if (!snapshot.exists) {
43+
unsubscribe();
44+
return reject(new Error("Export document not found"));
45+
}
46+
47+
const data = snapshot.data()!;
48+
49+
if (data.status === "complete") {
50+
unsubscribe();
51+
return resolve({
52+
storagePath: data.storagePath,
53+
zipPath: data.zipPath,
54+
});
55+
}
56+
});
57+
});
58+
59+
// Get a list of all the files in the export.
60+
const listResult = await listAll(ref(storage, storagePath));
61+
62+
// Store the paths to each exported file.
63+
setFiles(listResult.items.map(item => item.fullPath));
64+
65+
// We're done exporting.
66+
setExporting(false);
67+
}
68+
69+
async function onDownload(path: string) {
70+
// Download the file.
71+
const url = await getDownloadURL(ref(storage, path));
72+
73+
// Open the file in a new tab.
74+
window.open(url);
75+
}
76+
77+
if (!exporting && !files) {
78+
return (
79+
<Container>
80+
<div className={styles.buttonContainer}>
81+
<button type={'button'} className={styles.btn} onClick={() => onExport({ zip: true })}>Start Export (zip enabled)</button>
82+
<button type={'button'} className={styles.btn} onClick={() => onExport({ zip: false })}>Start Export (zip disabled)</button>
83+
</div>
84+
</Container>
85+
);
86+
}
87+
88+
if (exporting && !files) {
89+
return (
90+
<Container>
91+
<p>Exporting...</p>
92+
</Container>
93+
);
94+
}
95+
96+
return (
97+
<Container>
98+
<ol>
99+
{files!.map((file) => (
100+
<li key={file}>
101+
<span>{file}</span>
102+
<span>&nbsp; &nbsp;</span>
103+
<button onClick={() => onDownload(file)}>Download File</button>
104+
</li>
105+
))}
106+
</ol>
107+
</Container>
108+
);
109+
}
110+
111+
function Container(props: { children: React.ReactNode }) {
112+
return (
113+
<div className={styles.container}>
114+
<h1>Export User Data Demo</h1>
115+
<p>
116+
Click the export button below to trigger an export of some none user
117+
specific data. Once complete, you can download the exported items.
118+
</p>
119+
<div>{props.children}</div>
120+
</div>
121+
);
122+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
.container {
2+
font-family: Arial, Helvetica, sans-serif;
3+
max-width: 32rem;
4+
margin: 20px auto 0 auto;
5+
}
6+
7+
.buttonContainer {
8+
display: flex;
9+
justify-content: space-between;
10+
margin-top: 20px;
11+
flex-direction: column;
12+
height: 50%;
13+
padding: 0 20px;
14+
}
15+
16+
.btn {
17+
position: relative;
18+
padding: 20px 40px;
19+
display: block;
20+
margin: 30px auto;
21+
/* padding: 0; */
22+
cursor: pointer;
23+
overflow: hidden;
24+
25+
border-width: 0;
26+
outline: none;
27+
border-radius: 2px;
28+
box-shadow: 0 1px 4px rgba(0, 0, 0, .6);
29+
30+
background-color: #2ecc71;
31+
color: #ecf0f1;
32+
33+
transition: background-color .3s;
34+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/// <reference types="vite/client" />
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"compilerOptions": {
3+
"target": "ESNext",
4+
"useDefineForClassFields": true,
5+
"lib": ["DOM", "DOM.Iterable", "ESNext"],
6+
"allowJs": false,
7+
"skipLibCheck": true,
8+
"esModuleInterop": false,
9+
"allowSyntheticDefaultImports": true,
10+
"strict": true,
11+
"forceConsistentCasingInFileNames": true,
12+
"module": "ESNext",
13+
"moduleResolution": "Node",
14+
"resolveJsonModule": true,
15+
"isolatedModules": true,
16+
"noEmit": true,
17+
"jsx": "react-jsx"
18+
},
19+
"include": ["src"],
20+
"references": [{ "path": "./tsconfig.node.json" }]
21+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"compilerOptions": {
3+
"composite": true,
4+
"module": "ESNext",
5+
"moduleResolution": "Node",
6+
"allowSyntheticDefaultImports": true
7+
},
8+
"include": ["vite.config.ts"]
9+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { defineConfig } from "vite";
2+
import react from "@vitejs/plugin-react";
3+
4+
// https://vitejs.dev/config/
5+
export default defineConfig({
6+
plugins: [react()],
7+
});

0 commit comments

Comments
 (0)