Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix/for signal #193

Merged
merged 62 commits into from
Nov 8, 2024
Merged
Show file tree
Hide file tree
Changes from 60 commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
f4a3c88
DRY
IObert Oct 15, 2024
a89aea8
stabilize
IObert Oct 15, 2024
2252f9e
Improve templates and add sample order
IObert Oct 15, 2024
379e19d
chore: prettier
IObert Oct 15, 2024
21e2c2c
fix template
IObert Oct 15, 2024
e570f42
remove unneeded var
IObert Oct 15, 2024
be229ca
refactor for basic menu screen
IObert Oct 15, 2024
cab3e8e
shift layouts
IObert Oct 15, 2024
8f12c91
Add menu page draft
IObert Oct 16, 2024
3d859a8
chore: prettier
IObert Oct 16, 2024
843de67
fix SVG fill prop
IObert Oct 16, 2024
650afa5
fix svgs in a better way
IObert Oct 16, 2024
43fef93
render on server instead
IObert Oct 16, 2024
6660e8f
fix: slashes are not allowed for sync keys
IObert Oct 16, 2024
7918942
typo: template
IObert Oct 21, 2024
bea05d1
fix spelling to avoid "latte" issue
IObert Oct 22, 2024
574f3fa
more modifiers
IObert Oct 24, 2024
087d5e8
Download script to support param to download only one event key
IObert Oct 28, 2024
720c759
temp workaround for money2020
IObert Oct 28, 2024
f7d1b36
US as default over canada
IObert Oct 28, 2024
18afd31
hide some stages if lead collection is disabled
IObert Oct 28, 2024
cba9d73
list stages on chart
IObert Oct 28, 2024
8a2cf1e
test modifiers note
IObert Oct 28, 2024
ee2ecfd
sent more helper messages for modifiers
IObert Oct 28, 2024
38630e5
only send modifiers message if there are multiple modifiers
IObert Oct 28, 2024
be39c81
Add chek error script
IObert Oct 30, 2024
cfb4020
fail gracefully for invalid email input
IObert Oct 30, 2024
9c413d6
Fail gracefully when someones sends a media message
IObert Oct 30, 2024
d62491b
have one attendee file per event
IObert Oct 30, 2024
ccf6847
add "forget me" command to debug easier
IObert Oct 30, 2024
05aa686
Add a kiosk UI to order new items
IObert Oct 30, 2024
8e7c962
Add temp RCS resources
IObert Oct 30, 2024
1095677
undo latte workaround
IObert Oct 31, 2024
d8b3059
no error when all events are closed to keep error logs free
IObert Oct 31, 2024
4d9079c
Add kiosk explainer
IObert Oct 31, 2024
1574409
fix test because of renaming
IObert Oct 31, 2024
5a71a6f
removed short titles, adapt test
IObert Oct 31, 2024
fcda34e
add kiosk test
IObert Oct 31, 2024
2e9e603
make tests more robust
IObert Nov 4, 2024
27f808d
chore: update deps
IObert Nov 4, 2024
327cdc0
stablize tests
IObert Nov 4, 2024
8f8fcf4
unfreeze lockfile
IObert Nov 4, 2024
2e5c614
Revert "chore: update deps"
IObert Nov 4, 2024
6532e72
unfreeze
IObert Nov 4, 2024
d26b82d
refactor: uncomment and stabilize checkbox selection in e2e tests; up…
IObert Nov 4, 2024
63786f4
chore: update minor deps
IObert Nov 5, 2024
6fc73d1
chore: prettier
IObert Nov 5, 2024
1dfbd7e
add note about "forget me"
IObert Nov 5, 2024
28fc9c8
add iced coffee
IObert Nov 5, 2024
44a099c
add prompt for name / segment integration
IObert Nov 5, 2024
8f37930
cancel orders when users opts-out
IObert Nov 5, 2024
3e4bb99
fix build issue
IObert Nov 5, 2024
1f7b385
upgrade to next 15
IObert Nov 5, 2024
b266604
fix build issues
IObert Nov 5, 2024
2cc516c
fix vitest issue / prettier
IObert Nov 5, 2024
f193963
fix migration issues
IObert Nov 5, 2024
7784844
hide controls for guest users
IObert Nov 5, 2024
9152e12
adjust tests
IObert Nov 5, 2024
db8192b
fix client-side warnings
IObert Nov 5, 2024
d450766
add icons
IObert Nov 5, 2024
650d395
disable latte macchiato for now
IObert Nov 8, 2024
a801e20
Replace Latte Macchiato with Caffe Latte in test
IObert Nov 8, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
MIXOLOGIST_LOGIN=someuser:somepassword
ADMIN_LOGIN=someadmin:otherpassword
KIOSK_LOGIN=somekiosk:somepassword
SERVICE_INSTANCE_PREFIX=Mixologist
UNLIMITED_ORDERS=CommaSeparatedNumbersToWhichTheLimitDoesNotApply

Expand All @@ -13,4 +14,9 @@ TWILIO_VERIFY_SERVICE_SID=
TWILIO_CONVERSATIONS_SERVICE_SID=

# NGROK URL GOES HERE
PUBLIC_BASE_URL=
PUBLIC_BASE_URL=

#Optional
SEGMENT_SPACE_ID="your_segment_space_id"
SEGMENT_PROFILE_KEY="your_segment_profile_key"
SEGMENT_TRAIT_CHECK="your_segment_trait_check"
1 change: 1 addition & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ jobs:
TWILIO_CONVERSATIONS_SERVICE_SID: ${{ vars.TWILIO_CONVERSATIONS_SERVICE_SID }}
MIXOLOGIST_LOGIN: ${{ vars.MIXOLOGIST_LOGIN }}
ADMIN_LOGIN: ${{ vars.ADMIN_LOGIN }}
KIOSK_LOGIN: ${{ vars.KIOSK_LOGIN }}
- uses: actions/upload-artifact@v4
if: always()
with:
Expand Down
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,5 @@ package-lock.json
/playwright/.cache/


attendees.csv

attendees-*.csv
2 changes: 1 addition & 1 deletion CODE_OF_CONDUCT.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,4 @@ members of the project's leadership.
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html

[homepage]: https://www.contributor-covenant.org
[homepage]: https://www.contributor-covenant.org
91 changes: 59 additions & 32 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,50 +8,49 @@
Twilio Mixologist is an application that allows you to solve the problem of long queues at stands at events. Attendees can order their coffee, smoothie or whatever you serve via Twilio-powered channels, Mixologists get all orders on a website that can be accessed via a tablet and once an order is done the attendee will be notified via the system to come and pick it up. No more queueing and efficient coffee ☕️ ordering! 🎉

If you want to learn more about how this project was started, check out the this blog post:
> [Serving Coffee with Twilio Programmable SMS and React](https://www.twilio.com/en-us/blog/serving-coffee-with-sms-and-react-html)

Different versions of this system have been used at events such as:

* [NDC Oslo](https://ndcoslo.com) 2016, 2017
* [CSSConf EU](https://2017.cssconf.eu/) && [JSConf EU](https://2017.jsconf.eu/) 2017
* [WeAreDevelopers World Congress](https://www.wearedevelopers.com/world-congress) 2023, 2024
* [Mobile World Congress Barcelona](https://www.mwcbarcelona.com/) 2023, 2024
* [Money 20/20](https://www.money2020.com/) 2023
* [Twilio SIGNAL](https://signal.twilio.com/) 2023, 2024
> [Serving Coffee with Twilio Programmable SMS and React](https://www.twilio.com/en-us/blog/serving-coffee-with-sms-and-react-html)

Different versions of this system have been used at events such as:

- [NDC Oslo](https://ndcoslo.com) 2016, 2017
- [CSSConf EU](https://2017.cssconf.eu/) && [JSConf EU](https://2017.jsconf.eu/) 2017
- [WeAreDevelopers World Congress](https://www.wearedevelopers.com/world-congress) 2023, 2024
- [Mobile World Congress Barcelona](https://www.mwcbarcelona.com/) 2023, 2024
- [Money 20/20](https://www.money2020.com/) 2023
- [Twilio SIGNAL](https://signal.twilio.com/) 2023, 2024

## Features

* Receive orders using [Twilio Messaging]
* Store orders and real-time synchronization them between back-end and front-end using [Twilio Sync]
* Easy dynamic application configuration using [Twilio Sync]
* Managing message threads using [Twilio Conversations]
* Permission management based on [Twilio Sync]
* Easy way to reset the application from the admin interface
* Support multiple events that happen in parallel
* Query for location in the queue as well as canceling the order as a user
* All combined into a single [NextJS](https://nextjs.org/) web application
- Receive orders using [Twilio Messaging]
- Store orders and real-time synchronization them between back-end and front-end using [Twilio Sync]
- Easy dynamic application configuration using [Twilio Sync]
- Managing message threads using [Twilio Conversations]
- Permission management based on [Twilio Sync]
- Easy way to reset the application from the admin interface
- Support multiple events that happen in parallel
- Query for location in the queue as well as canceling the order as a user
- All combined into a single [NextJS](https://nextjs.org/) web application

### Pending Features

- [ ] Integration with Segment
- [ ] Your suggestions

### Channels

The current [Twilio Channels] are:

* [WhatsApp][twilio whatsapp]
* [SMS][twilio messaging]

- [WhatsApp][twilio whatsapp]
- [SMS][twilio messaging]

## Setup

### Requirements

* [Node.js] version 20 or higher
* [pnpm]
* A Twilio account - [Sign up here](https://www.twilio.com/try-twilio)
- [Node.js] version 20 or higher
- [pnpm]
- A Twilio account - [Sign up here](https://www.twilio.com/try-twilio)

## Setup

Expand All @@ -65,6 +64,7 @@ The current [Twilio Channels] are:
# Application related values
MIXOLOGIST_LOGIN=someuser:assword
ADMIN_LOGIN=someadmin:password
KIOSK_LOGIN=somekiosk:somepassword
SERVICE_INSTANCE_PREFIX=Mixologist
ACTIVE_CUSTOMERS_MAP=ActiveCustomers
UNLIMITED_ORDERS=CommaSeparatedNumbersToWhichTheLimitDoesNotApply
Expand All @@ -76,6 +76,10 @@ The current [Twilio Channels] are:
TWILIO_ACCOUNT_SID=
TWILIO_API_KEY=
TWILIO_API_SECRET=

SEGMENT_SPACE_ID="your_segment_space_id"
SEGMENT_PROFILE_KEY="your_segment_profile_key"
SEGMENT_TRAIT_CHECK="your_segment_trait_check"
```

Go into the [Twilio Console] and [generate an API Key and Secret](https://www.twilio.com/console/dev-tools/api-keys). Make sure to store the information safely.
Expand Down Expand Up @@ -109,6 +113,10 @@ pnpm dev

9. Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.

## How To Use

[Here's a diagram of what happens when the user sends a message to the application](resources/user-flow-diagram.png)

## Testing

This projects comes with a test suit that runs on every push to `main` and `feat/` branches. Unit tests cover basic capabilities (access control checks, template generator). And e2e tests cover the main functionality of the website since the data is loaded async and RSC's are currently not supported by unit test frameworks.
Expand All @@ -118,13 +126,33 @@ pnpm test # run unit tests
pnpm test:e2e # run e2e tests
```

## Deploy
## Optional Setup

TBD
### Tips for production

## How To Use
Here are a few helpful notes:

[Here's a diagram of what happens when the user sends a message to the application](resources/user-flow-diagram.png)
- If you are using the SMS channel, make sure to [set the SMS Geo Permissions](https://www.twilio.com/docs/messaging/guides/sms-geo-permissions)to make sure senders from the entire world can interact with the Mixologist.
- Edit the [opt-out management settings](https://help.twilio.com/articles/360034798533-Getting-Started-with-Advanced-Opt-Out-for-Messaging-Services) of the messaging service to avoid that users accidentally unsubscribe from the list.
- Regularly run `pnpm check-for-errors` and see if unforeseen errors occurred when the users tried to order.
- The Kiosk interface is a self-service interface that you can make available to attendees via a table or phone. The page allows the manual entry of an order without the need to put a phone number down. This form can be accessed via `https://<mixologist.server>/<event-slug>/kiosk` and the credentials are defined in the environment variable `KIOSK_LOGIN`.
- Users can send the command "forget me" to remove all data stored about this user. It cancels pending orders, removes the user from the Sync data store and removes the Conversation resource. This can be used for debugging as well as to be GDPR-compliant.

### Segment Integration

This project includes an optional integration with Segment's Profiles API. If you provide the `SEGMENT_SPACE_ID` and `SEGMENT_PROFILE_KEY` environment variables, the application will fetch user traits from Segment using the provided email address once the verification step is completed. The `SEGMENT_TRAIT_CHECK` environment variable allows you to specify a specific trait to check for in the user's profile.

To set up Segment integration:

1. **Create a Segment account** if you don't have one. Sign up [here](https://segment.com/).

2. **Create a Segment Space** and obtain your `SEGMENT_SPACE_ID`.

3. **Generate a Segment Profile API Key** and obtain your `SEGMENT_PROFILE_KEY`.

4. **Specify a Trait to Check** by setting the `SEGMENT_TRAIT_CHECK` environment variable to the desired trait key.

For more details on Segment and how to use the Profiles API, refer to the [Segment documentation](https://segment.com/docs/).

## Code of Conduct

Expand All @@ -136,13 +164,12 @@ All third party contributors acknowledge that any contributions they provide wil

## Icons Used

* [Mixologist Icons by Oliver Pitsch](https://www.smashingmagazine.com/2016/03/freebie-Mixologist-iconset-50-icons-eps-png-svg/)
* [Bar by BirVa Mehta from Noun Project](https://thenounproject.com/term/bar/1323725/)
- [Mixologist Icons by Oliver Pitsch](https://www.smashingmagazine.com/2016/03/freebie-Mixologist-iconset-50-icons-eps-png-svg/)
- [Bar by BirVa Mehta from Noun Project](https://thenounproject.com/term/bar/1323725/)

## License

MIT

MIT

[twilio console]: https://www.twilio.com/console
[twilio rest api]: https://www.twilio.com/docs/api/rest
Expand Down
5 changes: 1 addition & 4 deletions __tests__/e2e/browse-events.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ test.describe("[admin]", () => {

await page.goto("http://localhost:3000/event/test-event");

await page.waitForTimeout(1000);
await page.waitForTimeout(2000);

// select the first 10 items
for (let i = 1; i <= 10; i++) {
Expand Down Expand Up @@ -248,9 +248,6 @@ test.describe("[admin]", () => {
await expect(
page.getByRole("heading", { name: "Colombia (Red like Twilio!)" }),
).toBeVisible();
await expect(
page.getByRole("heading", { name: "Short title: Colombia" }),
).toBeVisible();
await expect(page.getByText("Strawberry, Pineapple, Apple")).toBeVisible();

const createButton = page
Expand Down
31 changes: 13 additions & 18 deletions __tests__/e2e/browse-orders.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ test.describe("[no login]", () => {
page.getByText('Original Message - "A Delivered Order"'),
).toBeVisible();

await expect(page.getByTestId("pause-orders")).toBeDisabled();
await expect(page.getByTestId("pause-orders")).toBeHidden();
});

test("Scroll through extra orders", async ({ page }) => {
Expand All @@ -66,15 +66,15 @@ test.describe("[no login]", () => {

await expect(
page.getByRole("button", { name: "Send Message to all open" }),
).toBeDisabled();
).toBeHidden();
});

test("Custom Order usable", async ({ page, context }) => {
await page.goto("/event/test-event/orders");

await expect(
page.getByRole("button", { name: "Create a Manual Order" }),
).toBeDisabled();
).toBeHidden();
});
});

Expand Down Expand Up @@ -143,12 +143,10 @@ test.describe("[mixologist]", () => {
.getByRole("button", { name: "Send Message to all open" })
.click();
await page.getByPlaceholder("Type your message here...").fill("Hello test");

await page
.getByRole("button", { name: "Send Message", exact: true })
.click();
await expect(
page.getByRole("button", { name: "Sending...", exact: true }),
).toBeVisible();
.isEnabled();
});

test("Custom Order usable", async ({ page, context }) => {
Expand All @@ -165,18 +163,15 @@ test.describe("[mixologist]", () => {

await page.goto("/event/test-event/orders");

await page
.getByRole("button", { name: "Create a Manual Order" })
.click();
await page.getByPlaceholder("Attendee name").fill("Test Name");
await page.getByRole("button", { name: "Create a Manual Order" }).click();
await page.getByPlaceholder("Customer name").fill("Test Name");
await page.getByLabel("Order Item").click();
await page.getByLabel('Espresso', { exact: true }).click();
await page.getByPlaceholder("Without regular milk or similar...").fill("Test Notes");
await page.getByLabel("Espresso", { exact: true }).click();
await page
.getByPlaceholder("Without regular milk or similar...")
.fill("Test Notes");
await page
.getByRole("button", { name: "Create Order", exact: true })
.click();
await expect(
page.getByRole("button", { name: "Creating...", exact: true }),
).toBeVisible();
.isEnabled();
});
});
});
37 changes: 37 additions & 0 deletions __tests__/e2e/kiosk-render.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { test, expect, type Locator } from "@playwright/test";
import { Privilege } from "@/middleware";

test.describe("[no login]", () => {
test("Should not see the page", async ({ page }) => {
await page.goto("/event/test-event/kiosk");

await expect(page.getByText("Unauthorized")).toBeVisible();
});
});

test.describe("[kiosk]", () => {
test("Form should be visible", async ({ page, context }) => {
await context.addCookies([
{
name: "privilege",
value: Privilege.KIOSK,
url: "http://localhost:3000",
},
]);
context.setExtraHTTPHeaders({
Authorization: `Basic ${btoa(process.env.KIOSK_LOGIN || ":")}`,
});

await page.goto("/event/test-event/kiosk");

await page.getByPlaceholder("Customer name").fill("Test Name");
await page.getByLabel("Order Item").click();
await page.getByLabel("Espresso", { exact: true }).click();
await page
.getByPlaceholder("Without regular milk or similar...")
.fill("Test Notes");
await page
.getByRole("button", { name: "Create Order", exact: true })
.isEnabled();
});
});
5 changes: 2 additions & 3 deletions __tests__/e2e/order-terminal.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { test, expect, type Locator } from "@playwright/test";
import { Privilege } from "@/middleware";
import exp from "constants";

test.describe("[no login] ", () => {
test("Only elements with permissions should be visible / page 1/2", async ({
Expand Down Expand Up @@ -47,7 +46,7 @@ test.describe("[no login] ", () => {
page.getByText('Original Message - "A Delivered Order"'),
).toBeVisible();

await expect(page.getByTestId("pause-orders")).toBeDisabled();
await expect(page.getByTestId("pause-orders")).toBeHidden();
});

test("Only elements with permissions should be visible / page 2/2", async ({
Expand Down Expand Up @@ -94,7 +93,7 @@ test.describe("[no login] ", () => {
page.getByText('Original Message - "A Delivered Order"'),
).toBeVisible();

await expect(page.getByTestId("pause-orders")).toBeDisabled();
await expect(page.getByTestId("pause-orders")).toBeHidden();
});

test("Should show right header for page 2/4", async ({ page }) => {
Expand Down
18 changes: 11 additions & 7 deletions __tests__/global-tear-down.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,18 @@ import Axios from "axios";
async function globalTeardown(config: FullConfig) {
const baseURL = config?.webServer?.url || "http://localhost:3000";

const response = await Axios.delete(`${baseURL}/api/event/test-event`, {
headers: {
"Content-Type": "application/json",
Authorization: `Basic ${btoa(process.env.ADMIN_LOGIN || ":")}`,
},
});
try {
const response = await Axios.delete(`${baseURL}/api/event/test-event`, {
headers: {
"Content-Type": "application/json",
Authorization: `Basic ${btoa(process.env.ADMIN_LOGIN || ":")}`,
},
});

expect(response.status).toBe(204);
expect(response.status).toBe(204);
} catch (error) {
console.error("Error deleting event: ", error);
}
}

export default globalTeardown;
2 changes: 1 addition & 1 deletion __tests__/scripts/orderExtractor.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, expect, test } from "vitest";
import { getOrderItemFromMessage } from "@/lib/utils";
import { Event } from "@/app/event/[slug]/page";
import { Event } from "@/app/(master-layout)/event/[slug]/page";

const mockedEvent: Event = {
name: "test event",
Expand Down
Loading