├── .gitignore ├── LICENSE ├── README.md ├── assets ├── chrome-bug.css ├── components.css └── main.css ├── components ├── Features.js ├── Layout.js ├── Pricing.js ├── PricingSnippet.js ├── SEOMeta.js ├── Testimonials.js ├── icons │ ├── AnimIcon.js │ ├── DownArrow.js │ ├── Github.js │ ├── Google.js │ ├── LandingImageOne.js │ ├── Logo.js │ ├── LoomIcon.js │ ├── Open.js │ ├── ReactIcon.js │ ├── ReactIconDark.js │ ├── Stripe.js │ ├── StripeConnect.js │ └── Twitter.js └── ui │ ├── AdminNavbar │ ├── AdminDesktopNav.js │ ├── AdminMobileNav.js │ └── AdminNavItems.js │ ├── Button.js │ ├── Footer.js │ ├── Forms │ └── CampaignForm.js │ ├── Input │ ├── Input.js │ ├── Input.module.css │ └── index.js │ ├── LoadingDots │ ├── LoadingDots.js │ ├── LoadingDots.module.css │ └── index.js │ ├── Modal.js │ ├── Navbar.js │ ├── SetupProgress.js │ ├── SimpleNav.js │ └── StripeDisconnectNotice.js ├── emails └── inviteAffiliate.js ├── jsconfig.json ├── next.config.js ├── package-lock.json ├── package.json ├── pages ├── _app.js ├── _document.js ├── api │ ├── affiliates │ │ ├── accept-team-invite.js │ │ ├── check-team-invites.js │ │ ├── delete-team-user.js │ │ ├── get-team-data.js │ │ ├── get-team-details.js │ │ ├── get-team-name.js │ │ ├── get-team-usage.js │ │ └── invite.js │ ├── create-checkout-session.js │ ├── create-portal-link.js │ ├── customer-events.js │ ├── embedData.js │ ├── get-account-details.js │ ├── get-stripe-id.js │ ├── subscribe.js │ ├── v1 │ │ ├── campaign-details.js │ │ ├── convert-referral.js │ │ ├── record-impression.js │ │ └── verify-company.js │ └── webhooks.js ├── changelog.js ├── dashboard │ ├── [companyId] │ │ ├── affiliates │ │ │ ├── index.js │ │ │ └── invite.js │ │ ├── campaigns │ │ │ ├── [campaignId] │ │ │ │ ├── edit.js │ │ │ │ └── index.js │ │ │ ├── index.js │ │ │ └── new.js │ │ ├── index.js │ │ ├── settings.js │ │ └── setup │ │ │ ├── add.js │ │ │ ├── campaign.js │ │ │ ├── currency.js │ │ │ ├── index.js │ │ │ ├── stripe.js │ │ │ └── verify.js │ ├── add-company.js │ ├── create-team.js │ ├── index.js │ ├── plan.js │ └── stripe-verify.js ├── index.js ├── pricing.js ├── reset-password.js ├── signin.js └── signup.js ├── postcss.config.js ├── public ├── affiliate-screenshot.webp ├── android-chrome-192x192.png ├── android-chrome-256x256.png ├── apple-touch-icon.png ├── browserconfig.xml ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico ├── fonts │ ├── TTInterfaces-Bold.woff │ ├── TTInterfaces-Bold.woff2 │ ├── TTInterfaces-Medium.woff │ ├── TTInterfaces-Medium.woff2 │ ├── TTInterfaces-Regular.woff │ └── TTInterfaces-Regular.woff2 ├── invite-screenshot.webp ├── js │ └── reflio.min.js ├── mstile-150x150.png ├── og.png ├── platform-screenshot.webp ├── reflio-logo.png ├── safari-pinned-tab.svg ├── site.webmanifest ├── standard-embed.png └── testimonials │ ├── _thunk_.jpeg │ ├── briansaetre.jpeg │ ├── foliofed.jpeg │ └── maxwellcdavis.jpeg ├── schema.sql ├── scripts └── reflio.js ├── tailwind.config.js ├── utils ├── AffiliateContext.js ├── CampaignContext.js ├── CompanyContext.js ├── email-builder-inner.js ├── email-builder-server.js ├── helpers.js ├── sendEmail.js ├── setupStepCheck.js ├── stripe-client.js ├── stripe-helpers.js ├── stripe.js ├── supabase-admin.js ├── supabase-client.js ├── useDatabase.js └── useUser.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /.cache/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env.local 30 | .env.development.local 31 | .env.test.local 32 | .env.production.local 33 | 34 | # vercel 35 | .vercel 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | Reflio Logo 3 |
4 | 5 |

Reflio

6 | 7 |

Create a referral program without breaking the bank.

8 | 9 |

Reflio »

10 | 11 | > NOTE: Reflio is under active development. Please visit our website (Reflio.com) to stay up to date with the latest info about our go-live date, information on contributing and more. 12 | 13 | Reflio puts digital privacy first and is proudly open-source. All referrals are processed through European-owned infrastructure, and our company is registered in the UK. With Reflio, referrals located in the EU are automatically required to confirm their consent before a cookie is set. 14 | 15 | 16 | Reflio Dashboard Screenshot 17 | 18 | 19 | ## Features / USPs 20 | 21 | - Start a referral program for your SaaS in minutes 22 | - Track referrals for Stripe subscriptions or one-time payments 23 | - Cross sub-domain tracking 24 | - Automated GDPR & Privacy compliance for users located in the EU 25 | - Fast embed script (<13kb) 26 | - Free plan available. Pricing from $0/month (with a 9% commission fee) 27 | - One central dashboard for your affiliates (All of their campaigns from different companies/brands in one dashboard). 28 | 29 | ## Contributing / Developer Guide 30 | 31 | We will be releasing our guide on how to contribute to Reflio very soon. 32 | 33 | ## License 34 | 35 | [GPL-3.0 license](https://github.com/Reflio-com/reflio/blob/master/LICENSE) 36 | -------------------------------------------------------------------------------- /assets/chrome-bug.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Chrome has a bug with transitions on load since 2012! 3 | * 4 | * To prevent a "pop" of content, you have to disable all transitions until 5 | * the page is done loading. 6 | * 7 | * https://lab.laukstein.com/bug/input 8 | * https://twitter.com/timer150/status/1345217126680899584 9 | */ 10 | body.loading * { 11 | transition: none !important; 12 | } 13 | -------------------------------------------------------------------------------- /assets/components.css: -------------------------------------------------------------------------------- 1 | .fit { 2 | min-height: calc(100vh - 88px); 3 | } 4 | -------------------------------------------------------------------------------- /assets/main.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'TT Interfaces'; 3 | src: url(../public/fonts/TTInterfaces-Regular.woff2) format('woff2'), 4 | url(../public/fonts/TTInterfaces-Regular.woff) format('woff'); 5 | font-weight: 300; 6 | font-style: normal; 7 | font-display: block; 8 | } 9 | 10 | @font-face { 11 | font-family: 'TT Interfaces'; 12 | src: url(../public/fonts/TTInterfaces-Medium.woff2) format('woff2'), 13 | url(../public/fonts/TTInterfaces-Medium.woff) format('woff'); 14 | font-weight: 400; 15 | font-style: normal; 16 | font-display: block; 17 | } 18 | 19 | @font-face { 20 | font-family: 'TT Interfaces'; 21 | src: url(../public/fonts/TTInterfaces-Bold.woff2) format('woff2'), 22 | url(../public/fonts/TTInterfaces-Bold.woff) format('woff'); 23 | font-weight: 500; 24 | font-style: normal; 25 | font-display: block; 26 | } 27 | 28 | @tailwind base; 29 | @tailwind components; 30 | @tailwind utilities; 31 | 32 | .wrapper { 33 | @apply mx-auto w-11/12 clear-both max-w-screen-xl; 34 | } 35 | 36 | .wrapper-max { 37 | @apply max-w-screen-xl; 38 | } 39 | 40 | .wrapper-sm { 41 | @apply max-w-3xl 42 | } 43 | 44 | :root { 45 | --primary: #ffaf45; 46 | --primary-2: #e99c36; 47 | --primary-3: #cd811f; 48 | --secondary: #915ff2; 49 | --secondary-2: #7a48dd; 50 | --secondary-3: #2a1368; 51 | --text-primary: #1b1b1b; 52 | --text-secondary: #ffffff; 53 | 54 | --accents-0: #212529; 55 | --accents-1: #343a40; 56 | --accents-2: #495057; 57 | --accents-3: #868e96; 58 | --accents-4: #adb5bd; 59 | --accents-5: #ced4da; 60 | --accents-6: #dee2e6; 61 | --accents-7: #e9ecef; 62 | --accents-8: #f1f3f5; 63 | --accents-9: #f8f9fa;; 64 | --header-height: 80px; 65 | --vh100-offset: calc(100vh - var(--header-height)); 66 | } 67 | 68 | *, 69 | *:before, 70 | *:after { 71 | box-sizing: inherit; 72 | } 73 | 74 | .text-primary { 75 | color: var(--primary); 76 | } 77 | 78 | a.text-primary:hover { 79 | color: var(--primary-2); 80 | } 81 | 82 | .text-secondary { 83 | color: var(--secondary); 84 | } 85 | 86 | a.text-secondary:hover { 87 | color: var(--secondary-2); 88 | } 89 | 90 | a.bg-primary:hover { 91 | background-color: var(--primary-2); 92 | } 93 | 94 | .bg-dark { 95 | background-color: var(--secondary-3); 96 | } 97 | 98 | html { 99 | height: 100%; 100 | box-sizing: border-box; 101 | touch-action: manipulation; 102 | font-feature-settings: 'case' 1, 'rlig' 1, 'calt' 0; 103 | text-rendering: optimizeSpeed; 104 | -webkit-font-smoothing: antialiased; 105 | -moz-osx-font-smoothing: grayscale; 106 | font-family: "TT Interfaces", ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji"; 107 | } 108 | 109 | html, 110 | body { 111 | text-rendering: optimizeSpeed; 112 | -webkit-font-smoothing: antialiased; 113 | -moz-osx-font-smoothing: antialiased; 114 | color: var(--text-primary); 115 | } 116 | 117 | html, 118 | body { 119 | @apply tracking-tight; 120 | } 121 | 122 | body { 123 | position: relative; 124 | min-height: 100%; 125 | margin: 0; 126 | } 127 | 128 | body { 129 | @apply bg-gray-50; 130 | } 131 | 132 | p, a, span { 133 | letter-spacing: -0.025em; 134 | } 135 | 136 | a { 137 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 138 | } 139 | 140 | .customGradient { 141 | background-image: linear-gradient(to left bottom, #705de4, #6a58d9, #6554cf, #5f4fc4, #5a4bba, #4b50b7, #3d54b2, #3057ac, #1f60a6, #22679d, #336d93, #487188); 142 | } 143 | 144 | .animated { 145 | -webkit-animation-duration: 1s; 146 | animation-duration: 1s; 147 | -webkit-animation-duration: 1s; 148 | animation-duration: 1s; 149 | -webkit-animation-fill-mode: both; 150 | animation-fill-mode: both; 151 | } 152 | 153 | .fadeIn { 154 | -webkit-animation-name: fadeIn; 155 | animation-name: fadeIn; 156 | } 157 | 158 | @-webkit-keyframes fadeIn { 159 | from { 160 | opacity: 0; 161 | } 162 | 163 | to { 164 | opacity: 1; 165 | } 166 | } 167 | 168 | @keyframes fadeIn { 169 | from { 170 | opacity: 0; 171 | } 172 | 173 | to { 174 | opacity: 1; 175 | } 176 | } 177 | 178 | .height-screen-helper { 179 | height: var(--vh100-offset); 180 | } 181 | 182 | .content-block { 183 | @apply py-8 bg-white; 184 | } 185 | 186 | .content-block h1 { 187 | @apply mt-2 text-3xl font-extrabold tracking-tight sm:text-5xl; 188 | } 189 | 190 | .content-block h2 { 191 | @apply mt-2 text-2xl font-extrabold tracking-tight sm:text-2xl; 192 | } 193 | 194 | .content-block h3, .content-block h4 { 195 | @apply mt-2 text-xl font-extrabold tracking-tight sm:text-xl; 196 | } 197 | 198 | .content-block p { 199 | @apply mt-3 text-sm md:text-lg mt-3; 200 | } 201 | 202 | .content-block ul { 203 | @apply pl-5 mb-6 204 | } 205 | 206 | .content-block li { 207 | @apply mt-3 font-bold text-sm md:text-lg mt-1; 208 | } 209 | 210 | .content-block a { 211 | @apply mt-3 font-bold text-sm md:text-lg mt-4; 212 | } 213 | 214 | .hover-3d { 215 | @apply p-8 rounded-xl border-primary-2 border-8 bg-white shadow-xl; 216 | } 217 | 218 | .loom-sdk-record-overlay-shadow-root-id { 219 | display: block !important; 220 | } 221 | 222 | .gradient-bg { 223 | content: ""; 224 | background: linear-gradient(60deg,#1d638a,#2d7aa5,#1e648b,#1a6e9e,#1d638a,#1d798a,#1d8a63,#1d8a40); 225 | animation: animatedgradient 3s ease infinite alternate; 226 | background-size: 300% 300%; 227 | } 228 | 229 | @keyframes animatedgradient { 230 | 0% { 231 | background-position: 0 50%; 232 | } 233 | 50% { 234 | background-position: 100% 50%; 235 | } 236 | 100% { 237 | background-position: 50% 0; 238 | } 239 | } 240 | 241 | .pricing-button { 242 | box-shadow: 0 0 250px rgba(255,255,255,0.6); 243 | } 244 | 245 | .fancy-list li:before { 246 | content: '✓'; 247 | margin-right: 6px; 248 | } -------------------------------------------------------------------------------- /components/Features.js: -------------------------------------------------------------------------------- 1 | import { 2 | EyeOffIcon, 3 | LightningBoltIcon, 4 | SparklesIcon, 5 | CurrencyDollarIcon, 6 | GlobeIcon, 7 | CreditCardIcon, 8 | EyeIcon 9 | } from '@heroicons/react/outline'; 10 | 11 | export default function Features() { 12 | 13 | const features = [ 14 | { 15 | name: 'Pricing from $0/month', 16 | description: "We're indie hacker friendly. Being indie hackers ourselves, we know that all new projects start from $0 MRR. Reflio starts from just $0/month with a 9% commission per successful referral.", 17 | active: true, 18 | icon: CurrencyDollarIcon 19 | }, 20 | { 21 | name: 'Get started in minutes', 22 | description: 'Quickly connect your SaaS product to Reflio with pre-written code examples. You can instantly take advantage of word of mouth referrals to get higher quality sign ups to your app via your existing users.', 23 | active: true, 24 | icon: SparklesIcon 25 | }, 26 | { 27 | name: 'Subscriptions or one-time charges', 28 | description: 'Reflio works with both subscriptions and one-time payments in Stripe. Future subscription payments that came from a referral are handled automatically and re-collected in your dashboard.', 29 | active: true, 30 | icon: CreditCardIcon 31 | }, 32 | { 33 | name: 'Cross subdomain tracking', 34 | description: "Is your SaaS on a subdomain? Don't worry. We'll automatically track across your main domain to your sub domain with no extra work on your end.", 35 | active: true, 36 | icon: GlobeIcon 37 | }, 38 | { 39 | name: "Automated GDPR & Privacy compliance", 40 | description: 'All data processed through European-owned infrastructure, and our company is registered in the UK. With Reflio, referrals located in the EU are automatically required to confirm their consent before a cookie is set.', 41 | active: true, 42 | icon: EyeOffIcon 43 | }, 44 | { 45 | name: 'Our embed script is fast', 46 | description: "Being under <13kb, our embed code is up to 5x faster than some of our competitors, meaning we're better for your SEO than they are.", 47 | active: true, 48 | icon: LightningBoltIcon 49 | }, 50 | ]; 51 | 52 | return( 53 |
54 |
55 | {features.map((feature) => ( 56 |
57 |
58 | { 59 | feature.icon && 60 | 61 | } 62 |

{feature.name}

63 |
64 |
{feature.description}
65 |
66 | ))} 67 |
68 |
69 | ); 70 | } -------------------------------------------------------------------------------- /components/Layout.js: -------------------------------------------------------------------------------- 1 | import dynamic from 'next/dynamic'; 2 | import { useRouter } from 'next/router'; 3 | import Navbar from '@/components/ui/Navbar'; 4 | 5 | export default function Layout({ children, meta: pageMeta }) { 6 | const Toaster = dynamic(() => 7 | import("react-hot-toast").then((module) => module.Toaster) 8 | ); 9 | const Footer = dynamic(() => import('@/components/ui/Footer')); 10 | const AdminMobileNav = dynamic(() => import('@/components/ui/AdminNavbar/AdminMobileNav')); 11 | const AdminDesktopNav = dynamic(() => import('@/components/ui/AdminNavbar/AdminDesktopNav')); 12 | const SimpleNav = dynamic(() => import('@/components/ui/SimpleNav')); 13 | const StripeDisconnectNotice = dynamic(() => import('@/components/ui/StripeDisconnectNotice')); 14 | const router = useRouter(); 15 | let defaultPage = true; 16 | let dashboardPage = false; 17 | let simplePage = false; 18 | 19 | if(router.pathname.indexOf('/dashboard') === -1 && router.pathname.indexOf('/dashboard/add-company') === -1 && router.pathname.indexOf('/dashboard/create-team') === -1){ 20 | defaultPage = true; 21 | dashboardPage = false; 22 | simplePage = false; 23 | } 24 | 25 | if(router.pathname === '/dashboard/add-company' || router.pathname === '/dashboard/create-team'){ 26 | defaultPage = false; 27 | dashboardPage = false; 28 | simplePage = true; 29 | } 30 | 31 | if(router.pathname.indexOf('/dashboard') > -1 && simplePage !== true){ 32 | defaultPage = false; 33 | dashboardPage = true; 34 | simplePage = false; 35 | } 36 | 37 | return ( 38 | <> 39 | <> 40 | 66 | { 67 | defaultPage === true && 68 | 69 | } 70 | { 71 | simplePage === true && 72 | 73 | } 74 | { 75 | defaultPage === true ? 76 |
{children}
77 | : simplePage === true ? 78 |
{children}
79 | : dashboardPage === true ? 80 |
81 | 82 |
83 | 84 | 85 |
86 | <> 87 | {children} 88 | 89 |
90 |
91 |
92 | :
{children}
93 | } 94 | { 95 | defaultPage === true && 96 |