mirror of
https://github.com/stronk-dev/RandomChad.git
synced 2025-07-05 10:35:08 +02:00
✨ merch demo (NOT FUNCTIONAL YET)
This commit is contained in:
parent
ddce6f901e
commit
545971ec88
@ -1,8 +1,20 @@
|
|||||||
rules_version = '2';
|
rules_version = '2';
|
||||||
service cloud.firestore {
|
service firebase.storage {
|
||||||
match /databases/{database}/documents {
|
|
||||||
match /{document=**} {
|
match /b/{bucket}/o {
|
||||||
allow read, write: if false;
|
|
||||||
|
|
||||||
|
// Disallow all by default
|
||||||
|
match /{allPaths=**} {
|
||||||
|
allow read, write: if false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow creation if size is not too big and resource does not exist
|
||||||
|
match /api/{fileName} {
|
||||||
|
|
||||||
|
allow read: if true;
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -1 +1 @@
|
|||||||
12
|
16
|
@ -1,5 +1,6 @@
|
|||||||
const express = require( 'express' )
|
const express = require( 'express' )
|
||||||
const cors = require( 'cors' )
|
const cors = require( 'cors' )
|
||||||
|
const bodyParser = require('body-parser')
|
||||||
|
|
||||||
// CORS enabled express generator
|
// CORS enabled express generator
|
||||||
module.exports = f => {
|
module.exports = f => {
|
||||||
@ -10,6 +11,9 @@ module.exports = f => {
|
|||||||
// Enable CORS
|
// Enable CORS
|
||||||
app.use( cors( { origin: true } ) )
|
app.use( cors( { origin: true } ) )
|
||||||
|
|
||||||
|
// Enable body parser
|
||||||
|
app.use( bodyParser.json() )
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|
||||||
}
|
}
|
@ -5,6 +5,7 @@ const { setAvatar, resetAvatar } = require( '../integrations/avatar' )
|
|||||||
const { rocketeerFromRequest, multipleRocketeersFromRequest } = require( '../integrations/rocketeers' )
|
const { rocketeerFromRequest, multipleRocketeersFromRequest } = require( '../integrations/rocketeers' )
|
||||||
const { generateNewOutfit, setPrimaryOutfit, generateMultipleNewOutfits } = require( '../integrations/changingroom' )
|
const { generateNewOutfit, setPrimaryOutfit, generateMultipleNewOutfits } = require( '../integrations/changingroom' )
|
||||||
const { subscribe_address_to_notifications } = require( '../integrations/notifier' )
|
const { subscribe_address_to_notifications } = require( '../integrations/notifier' )
|
||||||
|
const { order_merch } = require( '../integrations/merch' )
|
||||||
|
|
||||||
// ///////////////////////////////
|
// ///////////////////////////////
|
||||||
// Specific Rocketeer instances
|
// Specific Rocketeer instances
|
||||||
@ -28,8 +29,12 @@ app.put( '/api/rocketeer/:id/outfits', setPrimaryOutfit )
|
|||||||
/* ///////////////////////////////
|
/* ///////////////////////////////
|
||||||
// Notification API
|
// Notification API
|
||||||
// /////////////////////////////*/
|
// /////////////////////////////*/
|
||||||
app.post( '/api/notifications/:address', subscribe_address_to_notifications )
|
app.post( '/api/notifications/:address', ( req, res ) => order_merch( req.body ) )
|
||||||
|
|
||||||
|
/* ///////////////////////////////
|
||||||
|
// Merch API
|
||||||
|
// /////////////////////////////*/
|
||||||
|
app.post( '/api/merch/order', subscribe_address_to_notifications )
|
||||||
|
|
||||||
// ///////////////////////////////
|
// ///////////////////////////////
|
||||||
// Static collection data
|
// Static collection data
|
||||||
|
@ -4,6 +4,7 @@ const { web2domain } = require( '../nft-media/rocketeer' )
|
|||||||
const { rocketeerFromRequest, multipleRocketeersFromRequest } = require( '../integrations/rocketeers' )
|
const { rocketeerFromRequest, multipleRocketeersFromRequest } = require( '../integrations/rocketeers' )
|
||||||
const { generateNewOutfit, setPrimaryOutfit, generateMultipleNewOutfits } = require( '../integrations/changingroom' )
|
const { generateNewOutfit, setPrimaryOutfit, generateMultipleNewOutfits } = require( '../integrations/changingroom' )
|
||||||
const { subscribe_address_to_notifications } = require( '../integrations/notifier' )
|
const { subscribe_address_to_notifications } = require( '../integrations/notifier' )
|
||||||
|
const { order_merch } = require( '../integrations/merch' )
|
||||||
|
|
||||||
////////////////////////////////
|
////////////////////////////////
|
||||||
// Specific Rocketeer instances
|
// Specific Rocketeer instances
|
||||||
@ -23,6 +24,11 @@ app.put( '/testnetapi/rocketeer/:id/outfits', setPrimaryOutfit )
|
|||||||
// /////////////////////////////*/
|
// /////////////////////////////*/
|
||||||
app.post( '/testnetapi/notifications/:address', subscribe_address_to_notifications )
|
app.post( '/testnetapi/notifications/:address', subscribe_address_to_notifications )
|
||||||
|
|
||||||
|
/* ///////////////////////////////
|
||||||
|
// Merch API
|
||||||
|
// /////////////////////////////*/
|
||||||
|
app.post( '/testnetapi/merch/order', order_merch )
|
||||||
|
|
||||||
// Collection data
|
// Collection data
|
||||||
app.get( '/testnetapi/collection', async ( req, res ) => res.json( {
|
app.get( '/testnetapi/collection', async ( req, res ) => res.json( {
|
||||||
totalSupply: await getTotalSupply( 'rinkeby' ).catch( f => 'error' ),
|
totalSupply: await getTotalSupply( 'rinkeby' ).catch( f => 'error' ),
|
||||||
|
189
functions/integrations/merch.js
Normal file
189
functions/integrations/merch.js
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
const functions = require( 'firebase-functions' )
|
||||||
|
const { printapi } = functions.config()
|
||||||
|
const { db, dataFromSnap } = require( '../modules/firebase' )
|
||||||
|
const { log } = require( '../modules/helpers' )
|
||||||
|
const fetch = require( 'isomorphic-fetch' )
|
||||||
|
|
||||||
|
/* ///////////////////////////////
|
||||||
|
// API handlers
|
||||||
|
// /////////////////////////////*/
|
||||||
|
async function call_printapi( endpoint, data, method='POST', format='json', authenticated=true ) {
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
log( `Call requested: ${method}/${format} ${endpoint} with `, data )
|
||||||
|
|
||||||
|
// Format url, if it has https use the link as provided
|
||||||
|
const url = endpoint.includes( 'https://' ) ? endpoint : `${ printapi.base_url }${ endpoint }`
|
||||||
|
|
||||||
|
const access_token = authenticated && await get_auth_token()
|
||||||
|
if( authenticated ) log( `Found access token: `, access_token.slice( 0, 10 ) )
|
||||||
|
if( authenticated && !access_token ) throw new Error( `No access_token found` )
|
||||||
|
|
||||||
|
// Generate headers based on input
|
||||||
|
const headers = {
|
||||||
|
...( format == 'json' && data && { 'Content-Type': 'application/json' } ),
|
||||||
|
...( format == 'form' && data && { 'Content-Type': 'application/x-www-form-urlencoded' } ),
|
||||||
|
...( authenticated && { Authorization: `Bearer ${ access_token }` } )
|
||||||
|
}
|
||||||
|
log( `Headers `, headers )
|
||||||
|
// Generate data body
|
||||||
|
let body = {}
|
||||||
|
|
||||||
|
// fetch expects json to be stringified
|
||||||
|
if( format == 'json' ) body = JSON.stringify( data )
|
||||||
|
|
||||||
|
// Formdata being formdata
|
||||||
|
if( format == 'form' ) body = new URLSearchParams( data )
|
||||||
|
|
||||||
|
// Focmat fetch options
|
||||||
|
const options = {
|
||||||
|
method,
|
||||||
|
headers,
|
||||||
|
body
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call api
|
||||||
|
log( `Calling ${ url }`, )
|
||||||
|
const response = await fetch( url, options ).then( res => res.json( ) )
|
||||||
|
log( `Received `, response )
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
} catch( e ) {
|
||||||
|
|
||||||
|
console.error( `Error calling printapi: `, e )
|
||||||
|
return {
|
||||||
|
error: e.message
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
async function get_auth_token( ) {
|
||||||
|
|
||||||
|
const token_grace_period = 1000 * 60
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
// Get cached token
|
||||||
|
let { expires=0, access_token } = await db.collection( 'secrets' ).doc( 'printapi' ).get( ).then( dataFromSnap )
|
||||||
|
log( `Old access token: `, access_token && access_token.slice( 0, 10 ) )
|
||||||
|
|
||||||
|
// If token is still valid
|
||||||
|
if( ( expires - token_grace_period ) > Date.now() ) {
|
||||||
|
log( `Old access token still valid` )
|
||||||
|
return access_token
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grab new token and save it
|
||||||
|
log( `Requesting new token` )
|
||||||
|
const credentials = {
|
||||||
|
grant_type: 'client_credentials',
|
||||||
|
client_id: printapi.client_credentials,
|
||||||
|
client_secret: printapi.client_secret
|
||||||
|
}
|
||||||
|
const { access_token: new_access_token, expires_in } = await call_printapi( `/v2/oauth`, credentials, 'POST', 'form', false )
|
||||||
|
|
||||||
|
log( `New access token: `, new_access_token && new_access_token.slice( 0, 10 ) )
|
||||||
|
await db.collection( 'secrets' ).doc( 'printapi' ).set( {
|
||||||
|
access_token: new_access_token,
|
||||||
|
// expires_in is in seconds
|
||||||
|
expires: Date.now() + ( expires_in * 1000 )
|
||||||
|
}, { merge: true } )
|
||||||
|
|
||||||
|
return new_access_token
|
||||||
|
|
||||||
|
|
||||||
|
} catch( e ) {
|
||||||
|
|
||||||
|
console.error( `Error getting auth token `, e )
|
||||||
|
return false
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ///////////////////////////////
|
||||||
|
// Order flow functionality
|
||||||
|
// /////////////////////////////*/
|
||||||
|
async function make_printapi_order ( { image_url, product_id, quantity=1, address={}, email } ) {
|
||||||
|
|
||||||
|
// Demo data
|
||||||
|
// email = 'info@rocketeer.fans'
|
||||||
|
// image_url = 'https://storage.googleapis.com/rocketeer-nft.appspot.com/mainnetRocketeers/1.jpg'
|
||||||
|
// product_id = 'kurk_20x20'
|
||||||
|
// address = {
|
||||||
|
// "address": {
|
||||||
|
// "name": "John Doe",
|
||||||
|
// "line1": "Osloweg 75",
|
||||||
|
// "postCode": "9700 GE",
|
||||||
|
// "city": "Groningen",
|
||||||
|
// "country": "NL"
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
// Validations
|
||||||
|
if( !email || !image_url || !product_id ) throw new Error( `Missing order data` )
|
||||||
|
if( Object.keys( address ).length != 5 ) throw new Error( `Malformed address` )
|
||||||
|
|
||||||
|
// Make the order on printapi backenc
|
||||||
|
const order = {
|
||||||
|
email,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
productId: product_id,
|
||||||
|
quantity,
|
||||||
|
files: { content: image_url }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
shipping: {
|
||||||
|
address
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log( `Creating order: `, order )
|
||||||
|
const { checkout, ...details } = await call_printapi( `/v2/orders`, order )
|
||||||
|
log( `Order made with `, checkout, details )
|
||||||
|
|
||||||
|
// Generate pament link
|
||||||
|
const { paymentUrl, amount } = await call_printapi( checkout.setupUrl, {
|
||||||
|
billing: {
|
||||||
|
address
|
||||||
|
},
|
||||||
|
returnUrl: `https://tools.rocketeer.fans/#/merch/success/${ details.id }`
|
||||||
|
} )
|
||||||
|
|
||||||
|
return {
|
||||||
|
paymentUrl,
|
||||||
|
amount
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch( e ) {
|
||||||
|
|
||||||
|
return {
|
||||||
|
error: e.message
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.order_merch = async ( req, res ) => {
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
const { error, ...order } = await make_printapi_order( req.body )
|
||||||
|
if( error ) throw new Error( error )
|
||||||
|
|
||||||
|
return res.json( order )
|
||||||
|
|
||||||
|
} catch( e ) {
|
||||||
|
return res.json( {
|
||||||
|
error: e.message
|
||||||
|
} )
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -2,6 +2,10 @@
|
|||||||
// Helper functions
|
// Helper functions
|
||||||
// ///////////////////////////////
|
// ///////////////////////////////
|
||||||
|
|
||||||
|
exports.log = ( ...messages ) => {
|
||||||
|
if( process.env.development ) console.log( ...messages )
|
||||||
|
}
|
||||||
|
|
||||||
// Wait in async
|
// Wait in async
|
||||||
exports.wait = timeInMs => new Promise( resolve => setTimeout( resolve ), timeInMs )
|
exports.wait = timeInMs => new Promise( resolve => setTimeout( resolve ), timeInMs )
|
||||||
|
|
||||||
|
11415
functions/package-lock.json
generated
11415
functions/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -5,16 +5,17 @@
|
|||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"runtime": "firebase functions:config:get > .runtimeconfig.json",
|
"runtime": "firebase functions:config:get > .runtimeconfig.json",
|
||||||
"serve": "firebase emulators:start --only functions",
|
"serve": "firebase emulators:start --only functions",
|
||||||
"shell": "NODE_ENV=development firebase functions:shell",
|
"shell": "development=true firebase functions:shell",
|
||||||
"start": "NODE_ENV=development npm run shell",
|
"start": "development=true npm run shell",
|
||||||
"deploy": "firebase deploy --only functions",
|
"deploy": "firebase deploy --only functions",
|
||||||
"logs": "firebase functions:log"
|
"logs": "firebase functions:log"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "12"
|
"node": "16"
|
||||||
},
|
},
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"body-parser": "^1.19.1",
|
||||||
"color": "^4.0.2",
|
"color": "^4.0.2",
|
||||||
"color-namer": "^1.4.0",
|
"color-namer": "^1.4.0",
|
||||||
"convert-svg-to-jpeg": "^0.5.0",
|
"convert-svg-to-jpeg": "^0.5.0",
|
||||||
@ -23,7 +24,6 @@
|
|||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"firebase-admin": "^10.0.0",
|
"firebase-admin": "^10.0.0",
|
||||||
"firebase-functions": "^3.11.0",
|
"firebase-functions": "^3.11.0",
|
||||||
"form-data": "^4.0.0",
|
|
||||||
"isomorphic-fetch": "^3.0.0",
|
"isomorphic-fetch": "^3.0.0",
|
||||||
"jsdom": "^18.0.0",
|
"jsdom": "^18.0.0",
|
||||||
"juice": "^8.0.0",
|
"juice": "^8.0.0",
|
||||||
|
30607
minter/package-lock.json
generated
30607
minter/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -6,6 +6,7 @@
|
|||||||
"@testing-library/jest-dom": "^5.14.1",
|
"@testing-library/jest-dom": "^5.14.1",
|
||||||
"@testing-library/react": "^11.2.7",
|
"@testing-library/react": "^11.2.7",
|
||||||
"@testing-library/user-event": "^12.8.3",
|
"@testing-library/user-event": "^12.8.3",
|
||||||
|
"country-code-lookup": "^0.0.20",
|
||||||
"ethers": "^5.4.7",
|
"ethers": "^5.4.7",
|
||||||
"react": "^17.0.2",
|
"react": "^17.0.2",
|
||||||
"react-dom": "^17.0.2",
|
"react-dom": "^17.0.2",
|
||||||
|
@ -21,7 +21,7 @@ const Wrapper = styled.div`
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: ${ ( { align='center' } ) => align };
|
||||||
justify-content: ${ ( { justify='center' } ) => justify };
|
justify-content: ${ ( { justify='center' } ) => justify };
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -4,10 +4,11 @@ import Verifier from './organisms/Verifier'
|
|||||||
import Avatar from './organisms/Avatar'
|
import Avatar from './organisms/Avatar'
|
||||||
import Portfolio from './organisms/Portfolio'
|
import Portfolio from './organisms/Portfolio'
|
||||||
import Outfits from './organisms/Outfits'
|
import Outfits from './organisms/Outfits'
|
||||||
|
import Merch from './organisms/Merch'
|
||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { log } from '../modules/helpers'
|
import { log } from '../modules/helpers'
|
||||||
import { useAddress } from '../modules/web3'
|
import { useAddress } from '../modules/web3'
|
||||||
import { Routes, Route, useNavigate } from 'react-router-dom'
|
import { Routes, Route, useNavigate, useParams } from 'react-router-dom'
|
||||||
|
|
||||||
|
|
||||||
function Router() {
|
function Router() {
|
||||||
@ -57,6 +58,11 @@ function Router() {
|
|||||||
<Route path='/outfits/:rocketeerId' element={ <Outfits /> } />
|
<Route path='/outfits/:rocketeerId' element={ <Outfits /> } />
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
|
<Route path='/merch/' element={ <Merch /> }>
|
||||||
|
<Route path='/merch/success/:order_id' element={ <Merch /> } />
|
||||||
|
<Route path='/merch/:rocketeer_id' element={ <Merch /> } />
|
||||||
|
</Route>
|
||||||
|
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -145,3 +145,14 @@ export function useRocketeerImages() {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function make_merch_order( order ) {
|
||||||
|
|
||||||
|
return callApi( `/merch/order`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify( order )
|
||||||
|
} )
|
||||||
|
|
||||||
|
}
|
@ -1,5 +0,0 @@
|
|||||||
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
|
||||||
// allows you to do things like:
|
|
||||||
// expect(element).toHaveTextContent(/react/i)
|
|
||||||
// learn more: https://github.com/testing-library/jest-dom
|
|
||||||
import '@testing-library/jest-dom';
|
|
Loading…
x
Reference in New Issue
Block a user