├── .dockerignore ├── .editorconfig ├── .env.example ├── .gitattributes ├── .github ├── dependabot.yml └── workflows │ └── laravel.yml ├── .gitignore ├── .mergify.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── Makefile ├── app ├── Console │ └── Kernel.php ├── Exceptions │ └── Handler.php ├── Http │ ├── Controllers │ │ ├── CallController.php │ │ ├── Controller.php │ │ ├── DashboardController.php │ │ ├── TicketController.php │ │ └── TokenController.php │ ├── Kernel.php │ └── Middleware │ │ ├── Authenticate.php │ │ ├── EncryptCookies.php │ │ ├── TrustProxies.php │ │ └── VerifyCsrfToken.php ├── Providers │ ├── AppServiceProvider.php │ └── RouteServiceProvider.php ├── Ticket.php └── User.php ├── artisan ├── bootstrap ├── app.php └── cache │ └── .gitignore ├── composer.json ├── composer.lock ├── config ├── app.php ├── auth.php ├── cache.php ├── database.php ├── services.php ├── session.php └── view.php ├── database ├── .gitignore ├── migrations │ └── 2015_10_08_194513_create_tickets_table.php └── seeds │ ├── DatabaseSeeder.php │ └── TicketSeeder.php ├── docker-compose.yml ├── package-lock.json ├── package.json ├── phpunit.xml ├── public ├── .htaccess ├── favicon.ico ├── index.php └── robots.txt ├── readme.md ├── resources ├── img │ └── bicycle-polo.jpg ├── js │ ├── app.js │ ├── bootstrap.js │ └── browser-calls.js ├── sass │ ├── _variables.scss │ ├── app.scss │ └── bicycle-polo.scss └── views │ ├── _messages.blade.php │ ├── errors │ └── 503.blade.php │ ├── index.blade.php │ ├── layouts │ └── master.blade.php │ └── supportDashboard.blade.php ├── routes ├── channels.php ├── console.php └── web.php ├── server.php ├── storage ├── app │ ├── .gitignore │ └── public │ │ └── .gitignore ├── framework │ ├── .gitignore │ ├── cache │ │ ├── .gitignore │ │ └── data │ │ │ └── .gitignore │ ├── sessions │ │ └── .gitignore │ ├── testing │ │ └── .gitignore │ └── views │ │ └── .gitignore └── logs │ └── .gitignore ├── tests ├── CreatesApplication.php ├── Feature │ ├── CallControllerTest.php │ ├── ExampleTest.php │ ├── TicketControllerTest.php │ └── TokenControllerTest.php ├── TestCase.php └── Unit │ └── ExampleTest.php └── webpack.mix.js /.dockerignore: -------------------------------------------------------------------------------- 1 | vendor 2 | node_modules 3 | .sqlite 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 4 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [*.{yml,yaml}] 15 | indent_size = 2 16 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Laravel settings 2 | APP_ENV=local 3 | APP_DEBUG=true 4 | APP_KEY=SomeSuperSecretKey 5 | 6 | # Twilio API credentials 7 | # Found at https://www.twilio.com/user/account/voice 8 | TWILIO_ACCOUNT_SID=ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 9 | 10 | # Twilio phone number 11 | # Purchase one at https://www.twilio.com/user/account/phone-numbers 12 | TWILIO_NUMBER=+15552737123 13 | 14 | # SID of your TwiML Application 15 | # https://www.twilio.com/console/voice/twiml/apps 16 | # OR 17 | # twilio api:core:applications:create --friendly-name=voice-client-javascript 18 | TWILIO_APPLICATION_SID=APXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX 19 | 20 | # Your REST API Key information 21 | # https://www.twilio.com/console/project/api-keys 22 | # OR 23 | # twilio api:core:keys:create --friendly-name=voice-client-javascript -o json 24 | # NOTE: Make sure to copy the secret, it'll will only be displayed once 25 | API_KEY=SKXXXXXXXXXXXX 26 | API_SECRET=XXXXXXXXXXXXXX 27 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | *.css linguist-vendored 3 | *.scss linguist-vendored 4 | *.js linguist-vendored 5 | CHANGELOG.md export-ignore 6 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: composer 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 10 8 | ignore: 9 | - dependency-name: twilio/sdk 10 | versions: 11 | - 6.16.1 12 | -------------------------------------------------------------------------------- /.github/workflows/laravel.yml: -------------------------------------------------------------------------------- 1 | name: Laravel CI 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ${{ matrix.operating-system }} 8 | strategy: 9 | matrix: 10 | operating-system: [windows-latest, macos-latest, ubuntu-latest] 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v2 14 | 15 | - name: Setup Node 16 | uses: actions/setup-node@v1 17 | with: 18 | node-version: 12 19 | - name: Copy assets 20 | run: | 21 | npm install 22 | npm run production 23 | 24 | - name: Setup PHP 25 | uses: shivammathur/setup-php@v2 26 | with: 27 | php-version: 7.4 28 | extensions: mbstring, fileinfo, pdo_sqlite 29 | coverage: none 30 | 31 | - name: Install Dependencies (PHP vendors) 32 | run: composer install --dev -q --no-ansi --no-interaction --no-scripts --no-suggest --no-progress --prefer-dist 33 | - name: Copy ENV Laravel Configuration for CI 34 | run: composer run post-root-package-install 35 | - name: Generate key 36 | run: php artisan key:generate 37 | - name: Execute tests (Unit and Feature tests) 38 | run: php artisan test 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /public/hot 3 | /public/images 4 | /public/js 5 | /public/css 6 | /public/mix-manifest.json 7 | /public/storage 8 | /storage/*.key 9 | /vendor 10 | .env 11 | .env.backup 12 | .phpunit.result.cache 13 | Homestead.json 14 | Homestead.yaml 15 | npm-debug.log 16 | yarn-error.log 17 | -------------------------------------------------------------------------------- /.mergify.yml: -------------------------------------------------------------------------------- 1 | pull_request_rules: 2 | - name: automatic merge for Dependabot pull requests 3 | conditions: 4 | - author=dependabot-preview[bot] 5 | - status-success=build (macos-latest) 6 | - status-success=build (windows-latest) 7 | - status-success=build (ubuntu-latest) 8 | actions: 9 | merge: 10 | method: squash 11 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | - Using welcoming and inclusive language 18 | - Being respectful of differing viewpoints and experiences 19 | - Gracefully accepting constructive criticism 20 | - Focusing on what is best for the community 21 | - Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | - The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | - Trolling, insulting/derogatory comments, and personal or political attacks 28 | - Public or private harassment 29 | - Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | - Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at open-source@twilio.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Twilio 2 | 3 | All third party contributors acknowledge that any contributions they provide will be made under the same open source license that the open source project is provided under. 4 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM php:7.4 2 | 3 | WORKDIR /usr/src/app 4 | 5 | RUN apt-get update && \ 6 | apt-get upgrade -y && \ 7 | apt-get install -y git 8 | RUN apt-get install -y zip unzip 9 | 10 | COPY composer* ./ 11 | RUN curl --silent --show-error https://getcomposer.org/installer | php 12 | RUN mv composer.phar /usr/local/bin/composer 13 | 14 | 15 | COPY . . 16 | RUN make install 17 | 18 | EXPOSE 8000 19 | 20 | CMD [ "php", "artisan", "serve", "--host=0.0.0.0" ] 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Twilio Inc. 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. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: install serve 2 | 3 | install: 4 | touch database/database.sqlite 5 | composer install 6 | php artisan key:generate --force 7 | php artisan migrate --force 8 | php artisan db:seed --force 9 | 10 | serve-setup: 11 | php artisan serve --host=127.0.0.1 12 | open-browser: 13 | python3 -m webbrowser "http://127.0.0.1:8000"; 14 | serve: open-browser serve-setup 15 | -------------------------------------------------------------------------------- /app/Console/Kernel.php: -------------------------------------------------------------------------------- 1 | command('inspire')->hourly(); 28 | } 29 | 30 | /** 31 | * Register the commands for the application. 32 | * 33 | * @return void 34 | */ 35 | protected function commands() 36 | { 37 | $this->load(__DIR__.'/Commands'); 38 | 39 | require base_path('routes/console.php'); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/Exceptions/Handler.php: -------------------------------------------------------------------------------- 1 | dial(null, ['callerId'=>$callerIdNumber]); 24 | $phoneNumberToDial = $request->input('phoneNumber'); 25 | 26 | if (isset($phoneNumberToDial)) { 27 | $dial->number($phoneNumberToDial); 28 | } else { 29 | $dial->client('support_agent'); 30 | } 31 | 32 | return $response; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/Http/Controllers/Controller.php: -------------------------------------------------------------------------------- 1 | $tickets]); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/Http/Controllers/TicketController.php: -------------------------------------------------------------------------------- 1 | 'The :attribute is mandatory', 22 | 'phone_number.regex' => 'The phone number must be in E.164 format' 23 | ]; 24 | 25 | $this->validate( 26 | $request, [ 27 | 'name' => 'required', 28 | // E.164 format 29 | 'phone_number' => 'required|regex:/^\+[1-9]\d{1,14}$/', 30 | 'description' => 'required' 31 | ], $messages 32 | ); 33 | 34 | $newTicket = new Ticket($request->all()); 35 | $newTicket->save(); 36 | 37 | $request->session()->flash( 38 | 'status', 39 | "We've received your support ticket. We'll be in touch soon!" 40 | ); 41 | 42 | return redirect()->route('home'); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/Http/Controllers/TokenController.php: -------------------------------------------------------------------------------- 1 | accessToken=$accessToken; 17 | }/** 18 | * Create a new capability token 19 | * 20 | * @return \Illuminate\Http\Response 21 | */ 22 | public function newToken(Request $request) 23 | { 24 | $forPage = $request->input('forPage'); 25 | $accountSid = config('services.twilio')['accountSid']; 26 | $applicationSid = config('services.twilio')['applicationSid']; 27 | $apiKey = config('services.twilio')['apiKey']; 28 | $apiSecret = config('services.twilio')['apiSecret']; 29 | 30 | if ($forPage === route('dashboard', [], false)) { 31 | $this->accessToken->setIdentity('support_agent'); 32 | } else { 33 | $this->accessToken->setIdentity('customer'); 34 | } 35 | 36 | // Create Voice grant 37 | $voiceGrant = new VoiceGrant(); 38 | $voiceGrant->setOutgoingApplicationSid($applicationSid); 39 | 40 | // Optional: add to allow incoming calls 41 | $voiceGrant->setIncomingAllow(true); 42 | 43 | // Add grant to token 44 | $this->accessToken->addGrant($voiceGrant); 45 | 46 | // render token to string 47 | $token = $this->accessToken->toJWT(); 48 | 49 | return response()->json(['token' => $token]); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/Http/Kernel.php: -------------------------------------------------------------------------------- 1 | [ 32 | \App\Http\Middleware\EncryptCookies::class, 33 | \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, 34 | \Illuminate\Session\Middleware\StartSession::class, 35 | // \Illuminate\Session\Middleware\AuthenticateSession::class, 36 | \Illuminate\View\Middleware\ShareErrorsFromSession::class, 37 | \App\Http\Middleware\VerifyCsrfToken::class, 38 | \Illuminate\Routing\Middleware\SubstituteBindings::class, 39 | ], 40 | 41 | 'api' => [ 42 | 'throttle:60,1', 43 | \Illuminate\Routing\Middleware\SubstituteBindings::class, 44 | ], 45 | ]; 46 | 47 | /** 48 | * The application's route middleware. 49 | * 50 | * These middleware may be assigned to groups or used individually. 51 | * 52 | * @var array 53 | */ 54 | protected $routeMiddleware = [ 55 | 'auth' => \App\Http\Middleware\Authenticate::class, 56 | 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class, 57 | 'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class, 58 | 'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class, 59 | 'can' => \Illuminate\Auth\Middleware\Authorize::class, 60 | // 'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class, 61 | 'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class, 62 | 'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class, 63 | 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, 64 | 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class, 65 | ]; 66 | } 67 | -------------------------------------------------------------------------------- /app/Http/Middleware/Authenticate.php: -------------------------------------------------------------------------------- 1 | expectsJson()) { 18 | return route('login'); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/Http/Middleware/EncryptCookies.php: -------------------------------------------------------------------------------- 1 | app->bind( 19 | AccessToken::class, 20 | function ($app) { 21 | $accountSid = config('services.twilio')['accountSid']; 22 | $apiKey = config('services.twilio')['apiKey']; 23 | $apiSecret = config('services.twilio')['apiSecret']; 24 | 25 | return new AccessToken($accountSid, $apiKey, $apiSecret, 3600, 'identity'); 26 | } 27 | ); 28 | 29 | } 30 | 31 | /** 32 | * Bootstrap any application services. 33 | * 34 | * @return void 35 | */ 36 | public function boot() 37 | { 38 | // 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/Providers/RouteServiceProvider.php: -------------------------------------------------------------------------------- 1 | mapWebRoutes(); 47 | 48 | // 49 | } 50 | 51 | /** 52 | * Define the "web" routes for the application. 53 | * 54 | * These routes all receive session state, CSRF protection, etc. 55 | * 56 | * @return void 57 | */ 58 | protected function mapWebRoutes() 59 | { 60 | Route::middleware('web') 61 | ->namespace($this->namespace) 62 | ->group(base_path('routes/web.php')); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /app/Ticket.php: -------------------------------------------------------------------------------- 1 | 'datetime', 38 | ]; 39 | } 40 | -------------------------------------------------------------------------------- /artisan: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | make(Illuminate\Contracts\Console\Kernel::class); 34 | 35 | $status = $kernel->handle( 36 | $input = new Symfony\Component\Console\Input\ArgvInput, 37 | new Symfony\Component\Console\Output\ConsoleOutput 38 | ); 39 | 40 | /* 41 | |-------------------------------------------------------------------------- 42 | | Shutdown The Application 43 | |-------------------------------------------------------------------------- 44 | | 45 | | Once Artisan has finished running, we will fire off the shutdown events 46 | | so that any final work may be done by the application before we shut 47 | | down the process. This is the last thing to happen to the request. 48 | | 49 | */ 50 | 51 | $kernel->terminate($input, $status); 52 | 53 | exit($status); 54 | -------------------------------------------------------------------------------- /bootstrap/app.php: -------------------------------------------------------------------------------- 1 | singleton( 30 | Illuminate\Contracts\Http\Kernel::class, 31 | App\Http\Kernel::class 32 | ); 33 | 34 | $app->singleton( 35 | Illuminate\Contracts\Console\Kernel::class, 36 | App\Console\Kernel::class 37 | ); 38 | 39 | $app->singleton( 40 | Illuminate\Contracts\Debug\ExceptionHandler::class, 41 | App\Exceptions\Handler::class 42 | ); 43 | 44 | /* 45 | |-------------------------------------------------------------------------- 46 | | Return The Application 47 | |-------------------------------------------------------------------------- 48 | | 49 | | This script returns the application instance. The instance is given to 50 | | the calling script so we can separate the building of the instances 51 | | from the actual running of the application and sending responses. 52 | | 53 | */ 54 | 55 | return $app; 56 | -------------------------------------------------------------------------------- /bootstrap/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "twiliodeved/browser-calls-laravel", 3 | "type": "project", 4 | "description": "Browser Calls with Laravel.", 5 | "keywords": [ 6 | "framework", 7 | "laravel" 8 | ], 9 | "license": "MIT", 10 | "require": { 11 | "php": "^7.2.5", 12 | "fideloper/proxy": "^4.2", 13 | "fruitcake/laravel-cors": "^2.0", 14 | "guzzlehttp/guzzle": "^7.2", 15 | "laravel/framework": "^7.0", 16 | "laravel/tinker": "^2.0", 17 | "laravel/ui": "^2.0", 18 | "twilio/sdk": "^6.2.0" 19 | }, 20 | "require-dev": { 21 | "facade/ignition": "^2.0", 22 | "fzaninotto/faker": "^1.9.1", 23 | "mockery/mockery": "^1.3.1", 24 | "nunomaduro/collision": "^4.1", 25 | "phpunit/phpunit": "^8.5" 26 | }, 27 | "config": { 28 | "optimize-autoloader": true, 29 | "preferred-install": "dist", 30 | "sort-packages": true 31 | }, 32 | "extra": { 33 | "laravel": { 34 | "dont-discover": [] 35 | } 36 | }, 37 | "autoload": { 38 | "psr-4": { 39 | "App\\": "app/" 40 | }, 41 | "classmap": [ 42 | "database/seeds" 43 | ] 44 | }, 45 | "autoload-dev": { 46 | "psr-4": { 47 | "Tests\\": "tests/" 48 | } 49 | }, 50 | "minimum-stability": "dev", 51 | "prefer-stable": true, 52 | "scripts": { 53 | "post-autoload-dump": [ 54 | "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump", 55 | "@php artisan package:discover --ansi" 56 | ], 57 | "post-root-package-install": [ 58 | "@php -r \"file_exists('.env') || copy('.env.example', '.env');\"" 59 | ], 60 | "post-create-project-cmd": [ 61 | "@php artisan key:generate --ansi" 62 | ] 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /config/app.php: -------------------------------------------------------------------------------- 1 | env('APP_NAME', 'Laravel'), 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Application Environment 21 | |-------------------------------------------------------------------------- 22 | | 23 | | This value determines the "environment" your application is currently 24 | | running in. This may determine how you prefer to configure various 25 | | services the application utilizes. Set this in your ".env" file. 26 | | 27 | */ 28 | 29 | 'env' => env('APP_ENV', 'production'), 30 | 31 | /* 32 | |-------------------------------------------------------------------------- 33 | | Application Debug Mode 34 | |-------------------------------------------------------------------------- 35 | | 36 | | When your application is in debug mode, detailed error messages with 37 | | stack traces will be shown on every error that occurs within your 38 | | application. If disabled, a simple generic error page is shown. 39 | | 40 | */ 41 | 42 | 'debug' => (bool) env('APP_DEBUG', false), 43 | 44 | /* 45 | |-------------------------------------------------------------------------- 46 | | Application URL 47 | |-------------------------------------------------------------------------- 48 | | 49 | | This URL is used by the console to properly generate URLs when using 50 | | the Artisan command line tool. You should set this to the root of 51 | | your application so that it is used when running Artisan tasks. 52 | | 53 | */ 54 | 55 | 'url' => env('APP_URL', 'http://localhost'), 56 | 57 | 'asset_url' => env('ASSET_URL', null), 58 | 59 | /* 60 | |-------------------------------------------------------------------------- 61 | | Application Timezone 62 | |-------------------------------------------------------------------------- 63 | | 64 | | Here you may specify the default timezone for your application, which 65 | | will be used by the PHP date and date-time functions. We have gone 66 | | ahead and set this to a sensible default for you out of the box. 67 | | 68 | */ 69 | 70 | 'timezone' => 'UTC', 71 | 72 | /* 73 | |-------------------------------------------------------------------------- 74 | | Application Locale Configuration 75 | |-------------------------------------------------------------------------- 76 | | 77 | | The application locale determines the default locale that will be used 78 | | by the translation service provider. You are free to set this value 79 | | to any of the locales which will be supported by the application. 80 | | 81 | */ 82 | 83 | 'locale' => 'en', 84 | 85 | /* 86 | |-------------------------------------------------------------------------- 87 | | Application Fallback Locale 88 | |-------------------------------------------------------------------------- 89 | | 90 | | The fallback locale determines the locale to use when the current one 91 | | is not available. You may change the value to correspond to any of 92 | | the language folders that are provided through your application. 93 | | 94 | */ 95 | 96 | 'fallback_locale' => 'en', 97 | 98 | /* 99 | |-------------------------------------------------------------------------- 100 | | Faker Locale 101 | |-------------------------------------------------------------------------- 102 | | 103 | | This locale will be used by the Faker PHP library when generating fake 104 | | data for your database seeds. For example, this will be used to get 105 | | localized telephone numbers, street address information and more. 106 | | 107 | */ 108 | 109 | 'faker_locale' => 'en_US', 110 | 111 | /* 112 | |-------------------------------------------------------------------------- 113 | | Encryption Key 114 | |-------------------------------------------------------------------------- 115 | | 116 | | This key is used by the Illuminate encrypter service and should be set 117 | | to a random, 32 character string, otherwise these encrypted strings 118 | | will not be safe. Please do this before deploying an application! 119 | | 120 | */ 121 | 122 | 'key' => env('APP_KEY'), 123 | 124 | 'cipher' => 'AES-256-CBC', 125 | 126 | /* 127 | |-------------------------------------------------------------------------- 128 | | Autoloaded Service Providers 129 | |-------------------------------------------------------------------------- 130 | | 131 | | The service providers listed here will be automatically loaded on the 132 | | request to your application. Feel free to add your own services to 133 | | this array to grant expanded functionality to your applications. 134 | | 135 | */ 136 | 137 | 'providers' => [ 138 | 139 | /* 140 | * Laravel Framework Service Providers... 141 | */ 142 | Illuminate\Auth\AuthServiceProvider::class, 143 | Illuminate\Broadcasting\BroadcastServiceProvider::class, 144 | Illuminate\Bus\BusServiceProvider::class, 145 | Illuminate\Cache\CacheServiceProvider::class, 146 | Illuminate\Foundation\Providers\ConsoleSupportServiceProvider::class, 147 | Illuminate\Cookie\CookieServiceProvider::class, 148 | Illuminate\Database\DatabaseServiceProvider::class, 149 | Illuminate\Encryption\EncryptionServiceProvider::class, 150 | Illuminate\Filesystem\FilesystemServiceProvider::class, 151 | Illuminate\Foundation\Providers\FoundationServiceProvider::class, 152 | Illuminate\Hashing\HashServiceProvider::class, 153 | Illuminate\Mail\MailServiceProvider::class, 154 | Illuminate\Notifications\NotificationServiceProvider::class, 155 | Illuminate\Pagination\PaginationServiceProvider::class, 156 | Illuminate\Pipeline\PipelineServiceProvider::class, 157 | Illuminate\Queue\QueueServiceProvider::class, 158 | Illuminate\Redis\RedisServiceProvider::class, 159 | Illuminate\Auth\Passwords\PasswordResetServiceProvider::class, 160 | Illuminate\Session\SessionServiceProvider::class, 161 | Illuminate\Translation\TranslationServiceProvider::class, 162 | Illuminate\Validation\ValidationServiceProvider::class, 163 | Illuminate\View\ViewServiceProvider::class, 164 | 165 | /* 166 | * Package Service Providers... 167 | */ 168 | 169 | /* 170 | * Application Service Providers... 171 | */ 172 | App\Providers\AppServiceProvider::class, 173 | App\Providers\RouteServiceProvider::class, 174 | 175 | ], 176 | 177 | /* 178 | |-------------------------------------------------------------------------- 179 | | Class Aliases 180 | |-------------------------------------------------------------------------- 181 | | 182 | | This array of class aliases will be registered when this application 183 | | is started. However, feel free to register as many as you wish as 184 | | the aliases are "lazy" loaded so they don't hinder performance. 185 | | 186 | */ 187 | 188 | 'aliases' => [ 189 | 190 | 'App' => Illuminate\Support\Facades\App::class, 191 | 'Arr' => Illuminate\Support\Arr::class, 192 | 'Artisan' => Illuminate\Support\Facades\Artisan::class, 193 | 'Auth' => Illuminate\Support\Facades\Auth::class, 194 | 'Blade' => Illuminate\Support\Facades\Blade::class, 195 | 'Broadcast' => Illuminate\Support\Facades\Broadcast::class, 196 | 'Bus' => Illuminate\Support\Facades\Bus::class, 197 | 'Cache' => Illuminate\Support\Facades\Cache::class, 198 | 'Config' => Illuminate\Support\Facades\Config::class, 199 | 'Cookie' => Illuminate\Support\Facades\Cookie::class, 200 | 'Crypt' => Illuminate\Support\Facades\Crypt::class, 201 | 'DB' => Illuminate\Support\Facades\DB::class, 202 | 'Eloquent' => Illuminate\Database\Eloquent\Model::class, 203 | 'Event' => Illuminate\Support\Facades\Event::class, 204 | 'File' => Illuminate\Support\Facades\File::class, 205 | 'Gate' => Illuminate\Support\Facades\Gate::class, 206 | 'Hash' => Illuminate\Support\Facades\Hash::class, 207 | 'Http' => Illuminate\Support\Facades\Http::class, 208 | 'Lang' => Illuminate\Support\Facades\Lang::class, 209 | 'Log' => Illuminate\Support\Facades\Log::class, 210 | 'Mail' => Illuminate\Support\Facades\Mail::class, 211 | 'Notification' => Illuminate\Support\Facades\Notification::class, 212 | 'Password' => Illuminate\Support\Facades\Password::class, 213 | 'Queue' => Illuminate\Support\Facades\Queue::class, 214 | 'Redirect' => Illuminate\Support\Facades\Redirect::class, 215 | 'Redis' => Illuminate\Support\Facades\Redis::class, 216 | 'Request' => Illuminate\Support\Facades\Request::class, 217 | 'Response' => Illuminate\Support\Facades\Response::class, 218 | 'Route' => Illuminate\Support\Facades\Route::class, 219 | 'Schema' => Illuminate\Support\Facades\Schema::class, 220 | 'Session' => Illuminate\Support\Facades\Session::class, 221 | 'Storage' => Illuminate\Support\Facades\Storage::class, 222 | 'Str' => Illuminate\Support\Str::class, 223 | 'URL' => Illuminate\Support\Facades\URL::class, 224 | 'Validator' => Illuminate\Support\Facades\Validator::class, 225 | 'View' => Illuminate\Support\Facades\View::class, 226 | 227 | ], 228 | 229 | ]; 230 | -------------------------------------------------------------------------------- /config/auth.php: -------------------------------------------------------------------------------- 1 | [ 17 | 'guard' => 'web', 18 | 'passwords' => 'users', 19 | ], 20 | 21 | /* 22 | |-------------------------------------------------------------------------- 23 | | Authentication Guards 24 | |-------------------------------------------------------------------------- 25 | | 26 | | Next, you may define every authentication guard for your application. 27 | | Of course, a great default configuration has been defined for you 28 | | here which uses session storage and the Eloquent user provider. 29 | | 30 | | All authentication drivers have a user provider. This defines how the 31 | | users are actually retrieved out of your database or other storage 32 | | mechanisms used by this application to persist your user's data. 33 | | 34 | | Supported: "session", "token" 35 | | 36 | */ 37 | 38 | 'guards' => [ 39 | 'web' => [ 40 | 'driver' => 'session', 41 | 'provider' => 'users', 42 | ], 43 | 44 | 'api' => [ 45 | 'driver' => 'token', 46 | 'provider' => 'users', 47 | 'hash' => false, 48 | ], 49 | ], 50 | 51 | /* 52 | |-------------------------------------------------------------------------- 53 | | User Providers 54 | |-------------------------------------------------------------------------- 55 | | 56 | | All authentication drivers have a user provider. This defines how the 57 | | users are actually retrieved out of your database or other storage 58 | | mechanisms used by this application to persist your user's data. 59 | | 60 | | If you have multiple user tables or models you may configure multiple 61 | | sources which represent each model / table. These sources may then 62 | | be assigned to any extra authentication guards you have defined. 63 | | 64 | | Supported: "database", "eloquent" 65 | | 66 | */ 67 | 68 | 'providers' => [ 69 | 'users' => [ 70 | 'driver' => 'eloquent', 71 | 'model' => App\User::class, 72 | ], 73 | 74 | // 'users' => [ 75 | // 'driver' => 'database', 76 | // 'table' => 'users', 77 | // ], 78 | ], 79 | 80 | /* 81 | |-------------------------------------------------------------------------- 82 | | Resetting Passwords 83 | |-------------------------------------------------------------------------- 84 | | 85 | | You may specify multiple password reset configurations if you have more 86 | | than one user table or model in the application and you want to have 87 | | separate password reset settings based on the specific user types. 88 | | 89 | | The expire time is the number of minutes that the reset token should be 90 | | considered valid. This security feature keeps tokens short-lived so 91 | | they have less time to be guessed. You may change this as needed. 92 | | 93 | */ 94 | 95 | 'passwords' => [ 96 | 'users' => [ 97 | 'provider' => 'users', 98 | 'table' => 'password_resets', 99 | 'expire' => 60, 100 | 'throttle' => 60, 101 | ], 102 | ], 103 | 104 | /* 105 | |-------------------------------------------------------------------------- 106 | | Password Confirmation Timeout 107 | |-------------------------------------------------------------------------- 108 | | 109 | | Here you may define the amount of seconds before a password confirmation 110 | | times out and the user is prompted to re-enter their password via the 111 | | confirmation screen. By default, the timeout lasts for three hours. 112 | | 113 | */ 114 | 115 | 'password_timeout' => 10800, 116 | 117 | ]; 118 | -------------------------------------------------------------------------------- /config/cache.php: -------------------------------------------------------------------------------- 1 | env('CACHE_DRIVER', 'file'), 22 | 23 | /* 24 | |-------------------------------------------------------------------------- 25 | | Cache Stores 26 | |-------------------------------------------------------------------------- 27 | | 28 | | Here you may define all of the cache "stores" for your application as 29 | | well as their drivers. You may even define multiple stores for the 30 | | same cache driver to group types of items stored in your caches. 31 | | 32 | */ 33 | 34 | 'stores' => [ 35 | 36 | 'apc' => [ 37 | 'driver' => 'apc', 38 | ], 39 | 40 | 'array' => [ 41 | 'driver' => 'array', 42 | 'serialize' => false, 43 | ], 44 | 45 | 'database' => [ 46 | 'driver' => 'database', 47 | 'table' => 'cache', 48 | 'connection' => null, 49 | ], 50 | 51 | 'file' => [ 52 | 'driver' => 'file', 53 | 'path' => storage_path('framework/cache/data'), 54 | ], 55 | 56 | 'memcached' => [ 57 | 'driver' => 'memcached', 58 | 'persistent_id' => env('MEMCACHED_PERSISTENT_ID'), 59 | 'sasl' => [ 60 | env('MEMCACHED_USERNAME'), 61 | env('MEMCACHED_PASSWORD'), 62 | ], 63 | 'options' => [ 64 | // Memcached::OPT_CONNECT_TIMEOUT => 2000, 65 | ], 66 | 'servers' => [ 67 | [ 68 | 'host' => env('MEMCACHED_HOST', '127.0.0.1'), 69 | 'port' => env('MEMCACHED_PORT', 11211), 70 | 'weight' => 100, 71 | ], 72 | ], 73 | ], 74 | 75 | 'redis' => [ 76 | 'driver' => 'redis', 77 | 'connection' => 'cache', 78 | ], 79 | 80 | 'dynamodb' => [ 81 | 'driver' => 'dynamodb', 82 | 'key' => env('AWS_ACCESS_KEY_ID'), 83 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 84 | 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), 85 | 'table' => env('DYNAMODB_CACHE_TABLE', 'cache'), 86 | 'endpoint' => env('DYNAMODB_ENDPOINT'), 87 | ], 88 | 89 | ], 90 | 91 | /* 92 | |-------------------------------------------------------------------------- 93 | | Cache Key Prefix 94 | |-------------------------------------------------------------------------- 95 | | 96 | | When utilizing a RAM based store such as APC or Memcached, there might 97 | | be other applications utilizing the same cache. So, we'll specify a 98 | | value to get prefixed to all our keys so we can avoid collisions. 99 | | 100 | */ 101 | 102 | 'prefix' => env('CACHE_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_cache'), 103 | 104 | ]; 105 | -------------------------------------------------------------------------------- /config/database.php: -------------------------------------------------------------------------------- 1 | env('DB_CONNECTION', 'sqlite'), 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Database Connections 23 | |-------------------------------------------------------------------------- 24 | | 25 | | Here are each of the database connections setup for your application. 26 | | Of course, examples of configuring each database platform that is 27 | | supported by Laravel is shown below to make development simple. 28 | | 29 | | 30 | | All database work in Laravel is done through the PHP PDO facilities 31 | | so make sure you have the driver for your particular database of 32 | | choice installed on your machine before you begin development. 33 | | 34 | */ 35 | 36 | 'connections' => [ 37 | 38 | 'sqlite' => [ 39 | 'driver' => 'sqlite', 40 | 'database' => env('DB_DATABASE', database_path('database.sqlite')), 41 | 'prefix' => '', 42 | 'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true), 43 | ], 44 | 45 | 'mysql' => [ 46 | 'driver' => 'mysql', 47 | 'url' => env('DATABASE_URL'), 48 | 'host' => env('DB_HOST', '127.0.0.1'), 49 | 'port' => env('DB_PORT', '3306'), 50 | 'database' => env('DB_DATABASE', 'forge'), 51 | 'username' => env('DB_USERNAME', 'forge'), 52 | 'password' => env('DB_PASSWORD', ''), 53 | 'unix_socket' => env('DB_SOCKET', ''), 54 | 'charset' => 'utf8mb4', 55 | 'collation' => 'utf8mb4_unicode_ci', 56 | 'prefix' => '', 57 | 'prefix_indexes' => true, 58 | 'strict' => true, 59 | 'engine' => null, 60 | 'options' => extension_loaded('pdo_mysql') ? array_filter([ 61 | PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), 62 | ]) : [], 63 | ], 64 | 65 | 'pgsql' => [ 66 | 'driver' => 'pgsql', 67 | 'url' => env('DATABASE_URL'), 68 | 'host' => env('DB_HOST', '127.0.0.1'), 69 | 'port' => env('DB_PORT', '5432'), 70 | 'database' => env('DB_DATABASE', 'forge'), 71 | 'username' => env('DB_USERNAME', 'forge'), 72 | 'password' => env('DB_PASSWORD', ''), 73 | 'charset' => 'utf8', 74 | 'prefix' => '', 75 | 'prefix_indexes' => true, 76 | 'schema' => 'public', 77 | 'sslmode' => 'prefer', 78 | ], 79 | 80 | 'sqlsrv' => [ 81 | 'driver' => 'sqlsrv', 82 | 'url' => env('DATABASE_URL'), 83 | 'host' => env('DB_HOST', 'localhost'), 84 | 'port' => env('DB_PORT', '1433'), 85 | 'database' => env('DB_DATABASE', 'forge'), 86 | 'username' => env('DB_USERNAME', 'forge'), 87 | 'password' => env('DB_PASSWORD', ''), 88 | 'charset' => 'utf8', 89 | 'prefix' => '', 90 | 'prefix_indexes' => true, 91 | ], 92 | 93 | ], 94 | 95 | /* 96 | |-------------------------------------------------------------------------- 97 | | Migration Repository Table 98 | |-------------------------------------------------------------------------- 99 | | 100 | | This table keeps track of all the migrations that have already run for 101 | | your application. Using this information, we can determine which of 102 | | the migrations on disk haven't actually been run in the database. 103 | | 104 | */ 105 | 106 | 'migrations' => 'migrations', 107 | 108 | /* 109 | |-------------------------------------------------------------------------- 110 | | Redis Databases 111 | |-------------------------------------------------------------------------- 112 | | 113 | | Redis is an open source, fast, and advanced key-value store that also 114 | | provides a richer body of commands than a typical key-value system 115 | | such as APC or Memcached. Laravel makes it easy to dig right in. 116 | | 117 | */ 118 | 119 | 'redis' => [ 120 | 121 | 'client' => env('REDIS_CLIENT', 'phpredis'), 122 | 123 | 'options' => [ 124 | 'cluster' => env('REDIS_CLUSTER', 'redis'), 125 | 'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_database_'), 126 | ], 127 | 128 | 'default' => [ 129 | 'url' => env('REDIS_URL'), 130 | 'host' => env('REDIS_HOST', '127.0.0.1'), 131 | 'password' => env('REDIS_PASSWORD', null), 132 | 'port' => env('REDIS_PORT', '6379'), 133 | 'database' => env('REDIS_DB', '0'), 134 | ], 135 | 136 | 'cache' => [ 137 | 'url' => env('REDIS_URL'), 138 | 'host' => env('REDIS_HOST', '127.0.0.1'), 139 | 'password' => env('REDIS_PASSWORD', null), 140 | 'port' => env('REDIS_PORT', '6379'), 141 | 'database' => env('REDIS_CACHE_DB', '1'), 142 | ], 143 | 144 | ], 145 | 146 | ]; 147 | -------------------------------------------------------------------------------- /config/services.php: -------------------------------------------------------------------------------- 1 | [ 18 | 'domain' => env('MAILGUN_DOMAIN'), 19 | 'secret' => env('MAILGUN_SECRET'), 20 | 'endpoint' => env('MAILGUN_ENDPOINT', 'api.mailgun.net'), 21 | ], 22 | 23 | 'postmark' => [ 24 | 'token' => env('POSTMARK_TOKEN'), 25 | ], 26 | 27 | 'ses' => [ 28 | 'key' => env('AWS_ACCESS_KEY_ID'), 29 | 'secret' => env('AWS_SECRET_ACCESS_KEY'), 30 | 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'), 31 | ], 32 | 33 | 'twilio' => [ 34 | 'accountSid' => env('TWILIO_ACCOUNT_SID'), 35 | 'apiKey' => env('API_KEY'), 36 | 'apiSecret' => env('API_SECRET'), 37 | 'applicationSid' => env('TWILIO_APPLICATION_SID'), 38 | 'number' => env('TWILIO_NUMBER'), 39 | ], 40 | 41 | ]; 42 | -------------------------------------------------------------------------------- /config/session.php: -------------------------------------------------------------------------------- 1 | env('SESSION_DRIVER', 'file'), 22 | 23 | /* 24 | |-------------------------------------------------------------------------- 25 | | Session Lifetime 26 | |-------------------------------------------------------------------------- 27 | | 28 | | Here you may specify the number of minutes that you wish the session 29 | | to be allowed to remain idle before it expires. If you want them 30 | | to immediately expire on the browser closing, set that option. 31 | | 32 | */ 33 | 34 | 'lifetime' => env('SESSION_LIFETIME', 120), 35 | 36 | 'expire_on_close' => false, 37 | 38 | /* 39 | |-------------------------------------------------------------------------- 40 | | Session Encryption 41 | |-------------------------------------------------------------------------- 42 | | 43 | | This option allows you to easily specify that all of your session data 44 | | should be encrypted before it is stored. All encryption will be run 45 | | automatically by Laravel and you can use the Session like normal. 46 | | 47 | */ 48 | 49 | 'encrypt' => false, 50 | 51 | /* 52 | |-------------------------------------------------------------------------- 53 | | Session File Location 54 | |-------------------------------------------------------------------------- 55 | | 56 | | When using the native session driver, we need a location where session 57 | | files may be stored. A default has been set for you but a different 58 | | location may be specified. This is only needed for file sessions. 59 | | 60 | */ 61 | 62 | 'files' => storage_path('framework/sessions'), 63 | 64 | /* 65 | |-------------------------------------------------------------------------- 66 | | Session Database Connection 67 | |-------------------------------------------------------------------------- 68 | | 69 | | When using the "database" or "redis" session drivers, you may specify a 70 | | connection that should be used to manage these sessions. This should 71 | | correspond to a connection in your database configuration options. 72 | | 73 | */ 74 | 75 | 'connection' => env('SESSION_CONNECTION', null), 76 | 77 | /* 78 | |-------------------------------------------------------------------------- 79 | | Session Database Table 80 | |-------------------------------------------------------------------------- 81 | | 82 | | When using the "database" session driver, you may specify the table we 83 | | should use to manage the sessions. Of course, a sensible default is 84 | | provided for you; however, you are free to change this as needed. 85 | | 86 | */ 87 | 88 | 'table' => 'sessions', 89 | 90 | /* 91 | |-------------------------------------------------------------------------- 92 | | Session Cache Store 93 | |-------------------------------------------------------------------------- 94 | | 95 | | When using the "apc", "memcached", or "dynamodb" session drivers you may 96 | | list a cache store that should be used for these sessions. This value 97 | | must match with one of the application's configured cache "stores". 98 | | 99 | */ 100 | 101 | 'store' => env('SESSION_STORE', null), 102 | 103 | /* 104 | |-------------------------------------------------------------------------- 105 | | Session Sweeping Lottery 106 | |-------------------------------------------------------------------------- 107 | | 108 | | Some session drivers must manually sweep their storage location to get 109 | | rid of old sessions from storage. Here are the chances that it will 110 | | happen on a given request. By default, the odds are 2 out of 100. 111 | | 112 | */ 113 | 114 | 'lottery' => [2, 100], 115 | 116 | /* 117 | |-------------------------------------------------------------------------- 118 | | Session Cookie Name 119 | |-------------------------------------------------------------------------- 120 | | 121 | | Here you may change the name of the cookie used to identify a session 122 | | instance by ID. The name specified here will get used every time a 123 | | new session cookie is created by the framework for every driver. 124 | | 125 | */ 126 | 127 | 'cookie' => env( 128 | 'SESSION_COOKIE', 129 | Str::slug(env('APP_NAME', 'laravel'), '_').'_session' 130 | ), 131 | 132 | /* 133 | |-------------------------------------------------------------------------- 134 | | Session Cookie Path 135 | |-------------------------------------------------------------------------- 136 | | 137 | | The session cookie path determines the path for which the cookie will 138 | | be regarded as available. Typically, this will be the root path of 139 | | your application but you are free to change this when necessary. 140 | | 141 | */ 142 | 143 | 'path' => '/', 144 | 145 | /* 146 | |-------------------------------------------------------------------------- 147 | | Session Cookie Domain 148 | |-------------------------------------------------------------------------- 149 | | 150 | | Here you may change the domain of the cookie used to identify a session 151 | | in your application. This will determine which domains the cookie is 152 | | available to in your application. A sensible default has been set. 153 | | 154 | */ 155 | 156 | 'domain' => env('SESSION_DOMAIN', null), 157 | 158 | /* 159 | |-------------------------------------------------------------------------- 160 | | HTTPS Only Cookies 161 | |-------------------------------------------------------------------------- 162 | | 163 | | By setting this option to true, session cookies will only be sent back 164 | | to the server if the browser has a HTTPS connection. This will keep 165 | | the cookie from being sent to you if it can not be done securely. 166 | | 167 | */ 168 | 169 | 'secure' => env('SESSION_SECURE_COOKIE'), 170 | 171 | /* 172 | |-------------------------------------------------------------------------- 173 | | HTTP Access Only 174 | |-------------------------------------------------------------------------- 175 | | 176 | | Setting this value to true will prevent JavaScript from accessing the 177 | | value of the cookie and the cookie will only be accessible through 178 | | the HTTP protocol. You are free to modify this option if needed. 179 | | 180 | */ 181 | 182 | 'http_only' => true, 183 | 184 | /* 185 | |-------------------------------------------------------------------------- 186 | | Same-Site Cookies 187 | |-------------------------------------------------------------------------- 188 | | 189 | | This option determines how your cookies behave when cross-site requests 190 | | take place, and can be used to mitigate CSRF attacks. By default, we 191 | | will set this value to "lax" since this is a secure default value. 192 | | 193 | | Supported: "lax", "strict", "none", null 194 | | 195 | */ 196 | 197 | 'same_site' => 'lax', 198 | 199 | ]; 200 | -------------------------------------------------------------------------------- /config/view.php: -------------------------------------------------------------------------------- 1 | [ 17 | resource_path('views'), 18 | ], 19 | 20 | /* 21 | |-------------------------------------------------------------------------- 22 | | Compiled View Path 23 | |-------------------------------------------------------------------------- 24 | | 25 | | This option determines where all the compiled Blade templates will be 26 | | stored for your application. Typically, this is within the storage 27 | | directory. However, as usual, you are free to change this value. 28 | | 29 | */ 30 | 31 | 'compiled' => env( 32 | 'VIEW_COMPILED_PATH', 33 | realpath(storage_path('framework/views')) 34 | ), 35 | 36 | ]; 37 | -------------------------------------------------------------------------------- /database/.gitignore: -------------------------------------------------------------------------------- 1 | *.sqlite 2 | *.sqlite-journal 3 | -------------------------------------------------------------------------------- /database/migrations/2015_10_08_194513_create_tickets_table.php: -------------------------------------------------------------------------------- 1 | increments('id'); 19 | $table->string('name'); 20 | $table->string('phone_number'); 21 | $table->string('description'); 22 | 23 | $table->timestamps(); 24 | } 25 | ); 26 | } 27 | 28 | /** 29 | * Reverse the migrations. 30 | * 31 | * @return void 32 | */ 33 | public function down() 34 | { 35 | Schema::drop('tickets'); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /database/seeds/DatabaseSeeder.php: -------------------------------------------------------------------------------- 1 | call(TicketSeeder::class); 18 | 19 | Model::reguard(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /database/seeds/TicketSeeder.php: -------------------------------------------------------------------------------- 1 | 'Charles Holdsworth', 17 | 'phone_number' => '+14153674129', 18 | 'description' => 'I played for Middlesex in the championships and my ' . 19 | 'mallet squeaked the whole time! I demand a refund!'] 20 | ))->save(); 21 | 22 | (new Ticket( 23 | ['name' => 'John Woodger', 24 | 'phone_number' => '+15712812415', 25 | 'description' => 'The mallet you sold me broke! Call me immediately!'] 26 | ))->save(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.8" 2 | services: 3 | app: 4 | restart: always 5 | build: . 6 | ports: 7 | - "8000:8000" 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "dev": "npm run development", 5 | "development": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js", 6 | "watch": "npm run development -- --watch", 7 | "watch-poll": "npm run watch -- --watch-poll", 8 | "hot": "cross-env NODE_ENV=development node_modules/webpack-dev-server/bin/webpack-dev-server.js --inline --hot --config=node_modules/laravel-mix/setup/webpack.config.js", 9 | "prod": "npm run production", 10 | "production": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --no-progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js" 11 | }, 12 | "engines": { 13 | "node": "^12" 14 | }, 15 | "devDependencies": { 16 | "axios": "^0.19", 17 | "bootstrap": "^4.0.0", 18 | "cross-env": "^7.0", 19 | "jquery": "^3.2", 20 | "laravel-mix": "^5.0.1", 21 | "lodash": "^4.17.13", 22 | "popper.js": "^1.12", 23 | "resolve-url-loader": "^3.1.0", 24 | "sass": "^1.15.2", 25 | "sass-loader": "^8.0.0", 26 | "vue-template-compiler": "^2.6.11" 27 | }, 28 | "dependencies": { 29 | "twilio-client": "^1.10.1" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | ./tests/Unit 9 | 10 | 11 | ./tests/Feature 12 | 13 | 14 | 15 | 16 | ./app 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /public/.htaccess: -------------------------------------------------------------------------------- 1 | 2 | 3 | Options -MultiViews -Indexes 4 | 5 | 6 | RewriteEngine On 7 | 8 | # Handle Authorization Header 9 | RewriteCond %{HTTP:Authorization} . 10 | RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] 11 | 12 | # Redirect Trailing Slashes If Not A Folder... 13 | RewriteCond %{REQUEST_FILENAME} !-d 14 | RewriteCond %{REQUEST_URI} (.+)/$ 15 | RewriteRule ^ %1 [L,R=301] 16 | 17 | # Send Requests To Front Controller... 18 | RewriteCond %{REQUEST_FILENAME} !-d 19 | RewriteCond %{REQUEST_FILENAME} !-f 20 | RewriteRule ^ index.php [L] 21 | 22 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/browser-calls-laravel/8a7b6f098b18a316dc8c39d34e34408ecac76a30/public/favicon.ico -------------------------------------------------------------------------------- /public/index.php: -------------------------------------------------------------------------------- 1 | 8 | */ 9 | 10 | define('LARAVEL_START', microtime(true)); 11 | 12 | /* 13 | |-------------------------------------------------------------------------- 14 | | Register The Auto Loader 15 | |-------------------------------------------------------------------------- 16 | | 17 | | Composer provides a convenient, automatically generated class loader for 18 | | our application. We just need to utilize it! We'll simply require it 19 | | into the script here so that we don't have to worry about manual 20 | | loading any of our classes later on. It feels great to relax. 21 | | 22 | */ 23 | 24 | require __DIR__.'/../vendor/autoload.php'; 25 | 26 | /* 27 | |-------------------------------------------------------------------------- 28 | | Turn On The Lights 29 | |-------------------------------------------------------------------------- 30 | | 31 | | We need to illuminate PHP development, so let us turn on the lights. 32 | | This bootstraps the framework and gets it ready for use, then it 33 | | will load up this application so that we can run it and send 34 | | the responses back to the browser and delight our users. 35 | | 36 | */ 37 | 38 | $app = require_once __DIR__.'/../bootstrap/app.php'; 39 | 40 | /* 41 | |-------------------------------------------------------------------------- 42 | | Run The Application 43 | |-------------------------------------------------------------------------- 44 | | 45 | | Once we have the application, we can handle the incoming request 46 | | through the kernel, and send the associated response back to 47 | | the client's browser allowing them to enjoy the creative 48 | | and wonderful application we have prepared for them. 49 | | 50 | */ 51 | 52 | $kernel = $app->make(Illuminate\Contracts\Http\Kernel::class); 53 | 54 | $response = $kernel->handle( 55 | $request = Illuminate\Http\Request::capture() 56 | ); 57 | 58 | $response->send(); 59 | 60 | $kernel->terminate($request, $response); 61 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | 2 | Twilio 3 | 4 | 5 | # Browser Calls (Laravel) 6 | 7 | > This repository is now archived and is no longer being maintained. 8 | > Check out the [JavaScript SDK Quickstarts](https://www.twilio.com/docs/voice/sdks/javascript/get-started) to get started with browser-based calling. 9 | 10 | ## About 11 | 12 | Learn how to use [Twilio's JavaScript SDK](https://www.twilio.com/docs/voice/sdks/javascript) to make browser-to-phone and browser-to-browser calls with ease. The unsatisfied customers of the Birchwood Bicycle Polo Co. need your help! 13 | 14 | ## Set up 15 | 16 | ### Requirements 17 | 18 | - [PHP >= 7.2.5](https://www.php.net/) and [composer](https://getcomposer.org/) 19 | - [Node.js](https://nodejs.org/) 20 | - [SQLite](https://www.sqlite.org/index.html) 21 | - A Twilio account - [sign up](https://www.twilio.com/try-twilio) 22 | 23 | ### Twilio Account Settings 24 | 25 | This application should give you a ready-made starting point for writing your own application. 26 | Before we begin, we need to collect all the config values we need to run the application: 27 | 28 | | Config Value | Description | 29 | | :---------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------- | 30 | | Account Sid | Your primary Twilio account identifier - find this [in the Console](https://www.twilio.com/console). | 31 | | Phone number | A Twilio phone number in [E.164 format](https://en.wikipedia.org/wiki/E.164) - you can [get one here](https://www.twilio.com/console/phone-numbers/incoming) | 32 | | App Sid | The TwiML application with a voice URL configured to access your server running this app - create one [in the console here](https://www.twilio.com/console/voice/twiml/apps). Also, you will need to configure the Voice "REQUEST URL" on the TwiML app once you've got your server up and running. | 33 | | API Key / API Secret | Your REST API Key information needed to create an [Access Token](https://www.twilio.com/docs/iam/access-tokens) - create [one here](https://www.twilio.com/console/project/api-keys). | 34 | 35 | ### Create a TwiML App 36 | 37 | This project is configured to use a **TwiML App**, which allows us to easily set the voice URLs for all Twilio phone numbers we purchase in this app. 38 | 39 | Create a new TwiML app at https://www.twilio.com/console/voice/twiml/apps and use its `Sid` as the `TWILIO_APPLICATION_SID` environment variable wherever you run this app. 40 | 41 | Once you have created your TwiML app, configure your Twilio phone number to use it ([instructions here](https://www.twilio.com/help/faq/twilio-client/how-do-i-create-a-twiml-app)). 42 | 43 | If you don't have a Twilio phone number yet, you can purchase a new number in your [Twilio Account Dashboard](https://www.twilio.com/user/account/phone-numbers/incoming). 44 | 45 | ### Local development 46 | 47 | After the above requirements have been met: 48 | 49 | 1. Clone this repository and `cd` into it 50 | 51 | ```bash 52 | git clone git@github.com:TwilioDevEd/browser-calls-laravel.git 53 | cd browser-calls-laravel 54 | ``` 55 | 56 | 1. Install PHP dependencies 57 | 58 | ```bash 59 | make install 60 | ``` 61 | 62 | 1. Set your environment variables 63 | 64 | ```bash 65 | cp .env.example .env 66 | ``` 67 | 68 | See [Twilio Account Settings](#twilio-account-settings) to locate the necessary environment variables. 69 | 70 | 1. Install Node dependencies 71 | ```bash 72 | npm install 73 | ``` 74 | 75 | 1. Build the frontend assets 76 | ```bash 77 | npm run dev 78 | ``` 79 | 80 | 1. Run the application 81 | 82 | ```bash 83 | php artisan serve 84 | ``` 85 | 86 | 1. Run the application 87 | 88 | ```bash 89 | make serve 90 | ``` 91 | 92 | 1. Expose the application to the wider Internet using [ngrok](https://ngrok.com/) 93 | 94 | ```bash 95 | $ ngrok http 8000 96 | ``` 97 | Once you have started ngrok, update your TwiML app's voice URL setting to use your ngrok hostname, so it will look something like this: 98 | 99 | ``` 100 | https://.ngrok.io/support/call 101 | ``` 102 | 103 | 1. To create a support ticket go to the home page. 104 | On this page you could fill some fields and create a ticket or you can call to support. 105 | 106 | ``` 107 | https://.ngrok.io 108 | ``` 109 | 110 | __Note:__ Make sure you use the `https` version of your ngrok URL as some 111 | browsers won't allow access to the microphone unless you are using a secure 112 | SSL connection. 113 | 114 | 1. To respond to support tickets go to the `dashboard` page (you should open two windows or tabs). 115 | On this page you could call customers and answers phone calls. 116 | 117 | ``` 118 | https://.ngrok.io/dashboard 119 | ``` 120 | 121 | That's it! 122 | 123 | ### Docker 124 | 125 | If you have [Docker](https://www.docker.com/) already installed on your machine, you can use our `docker-compose.yml` to setup your project. 126 | 127 | 1. Make sure you have the project cloned. 128 | 2. Setup the `.env` file as outlined in the [Local Development](#local-development) steps. 129 | 3. Run `docker-compose up`. 130 | 4. Follow the steps in [Local Development](#local-development) on how to expose your port to Twilio using a tool like [ngrok](https://ngrok.com/) and configure the remaining parts of your application 131 | 132 | ### Unit and Integration Tests 133 | 134 | You can run the Unit and Feature tests locally by typing: 135 | ```bash 136 | php artisan test 137 | ``` 138 | 139 | ## Resources 140 | 141 | - The CodeExchange repository can be found [here](https://github.com/twilio-labs/code-exchange/). 142 | 143 | ## License 144 | 145 | [MIT](http://www.opensource.org/licenses/mit-license.html) 146 | 147 | ## Disclaimer 148 | 149 | No warranty expressed or implied. Software is as is. 150 | 151 | [twilio]: https://www.twilio.com 152 | -------------------------------------------------------------------------------- /resources/img/bicycle-polo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TwilioDevEd/browser-calls-laravel/8a7b6f098b18a316dc8c39d34e34408ecac76a30/resources/img/bicycle-polo.jpg -------------------------------------------------------------------------------- /resources/js/app.js: -------------------------------------------------------------------------------- 1 | require('./bootstrap'); 2 | -------------------------------------------------------------------------------- /resources/js/bootstrap.js: -------------------------------------------------------------------------------- 1 | window._ = require('lodash'); 2 | 3 | /** 4 | * We'll load jQuery and the Bootstrap jQuery plugin which provides support 5 | * for JavaScript based Bootstrap features such as modals and tabs. This 6 | * code may be modified to fit the specific needs of your application. 7 | */ 8 | 9 | try { 10 | window.Popper = require('popper.js').default; 11 | window.$ = window.jQuery = require('jquery'); 12 | 13 | require('bootstrap'); 14 | } catch (e) {} 15 | 16 | /** 17 | * We'll load the axios HTTP library which allows us to easily issue requests 18 | * to our Laravel back-end. This library automatically handles sending the 19 | * CSRF token as a header based on the value of the "XSRF" token cookie. 20 | */ 21 | 22 | window.axios = require('axios'); 23 | 24 | window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; 25 | 26 | /** 27 | * Echo exposes an expressive API for subscribing to channels and listening 28 | * for events that are broadcast by Laravel. Echo and event broadcasting 29 | * allows your team to easily build robust real-time web applications. 30 | */ 31 | 32 | // import Echo from 'laravel-echo'; 33 | 34 | // window.Pusher = require('pusher-js'); 35 | 36 | // window.Echo = new Echo({ 37 | // broadcaster: 'pusher', 38 | // key: process.env.MIX_PUSHER_APP_KEY, 39 | // cluster: process.env.MIX_PUSHER_APP_CLUSTER, 40 | // encrypted: true 41 | // }); 42 | -------------------------------------------------------------------------------- /resources/js/browser-calls.js: -------------------------------------------------------------------------------- 1 | import $ from "jquery"; 2 | const { Device } = require('twilio-client'); 3 | /** 4 | * Twilio Client configuration for the browser-calls-laravel 5 | * example application. 6 | */ 7 | 8 | // Store some selectors for elements we'll reuse 9 | var callStatus = $("#call-status"); 10 | var answerButton = $(".answer-button"); 11 | var callSupportButton = $(".call-support-button"); 12 | var hangUpButton = $(".hangup-button"); 13 | var callCustomerButtons = $(".call-customer-button"); 14 | 15 | var device = null; 16 | 17 | /* Helper function to update the call status bar */ 18 | function updateCallStatus(status) { 19 | callStatus.attr('placeholder', status); 20 | } 21 | 22 | /* Get a Twilio Client token with an AJAX request */ 23 | $(document).ready(function() { 24 | setupClient(); 25 | }); 26 | 27 | function setupHandlers(device) { 28 | device.on('ready', function (_device) { 29 | updateCallStatus("Ready"); 30 | }); 31 | 32 | /* Report any errors to the call status display */ 33 | device.on('error', function (error) { 34 | updateCallStatus("ERROR: " + error.message); 35 | }); 36 | 37 | /* Callback for when Twilio Client initiates a new connection */ 38 | device.on('connect', function (connection) { 39 | // Enable the hang up button and disable the call buttons 40 | hangUpButton.prop("disabled", false); 41 | callCustomerButtons.prop("disabled", true); 42 | callSupportButton.prop("disabled", true); 43 | answerButton.prop("disabled", true); 44 | 45 | // If phoneNumber is part of the connection, this is a call from a 46 | // support agent to a customer's phone 47 | if ("phoneNumber" in connection.message) { 48 | updateCallStatus("In call with " + connection.message.phoneNumber); 49 | } else { 50 | // This is a call from a website user to a support agent 51 | updateCallStatus("In call with support"); 52 | } 53 | }); 54 | 55 | /* Callback for when a call ends */ 56 | device.on('disconnect', function(connection) { 57 | // Disable the hangup button and enable the call buttons 58 | hangUpButton.prop("disabled", true); 59 | callCustomerButtons.prop("disabled", false); 60 | callSupportButton.prop("disabled", false); 61 | 62 | updateCallStatus("Ready"); 63 | }); 64 | 65 | /* Callback for when Twilio Client receives a new incoming call */ 66 | device.on('incoming', function(connection) { 67 | updateCallStatus("Incoming support call"); 68 | 69 | // Set a callback to be executed when the connection is accepted 70 | connection.accept(function() { 71 | updateCallStatus("In call with customer"); 72 | }); 73 | 74 | // Set a callback on the answer button and enable it 75 | answerButton.click(function() { 76 | connection.accept(); 77 | }); 78 | answerButton.prop("disabled", false); 79 | }); 80 | }; 81 | 82 | function setupClient() { 83 | $.post("/token", { 84 | forPage: window.location.pathname, 85 | _token: $('meta[name="csrf-token"]').attr('content') 86 | }).done(function (data) { 87 | // Set up the Twilio Client device with the token 88 | device = new Device(); 89 | device.setup(data.token); 90 | 91 | setupHandlers(device); 92 | }).fail(function () { 93 | updateCallStatus("Could not get a token from server!"); 94 | }); 95 | 96 | }; 97 | 98 | /* Call a customer from a support ticket */ 99 | window.callCustomer = function(phoneNumber) { 100 | updateCallStatus("Calling " + phoneNumber + "..."); 101 | 102 | var params = {"phoneNumber": phoneNumber}; 103 | device.connect(params); 104 | }; 105 | 106 | /* Call the support_agent from the home page */ 107 | window.callSupport = function() { 108 | updateCallStatus("Calling support..."); 109 | 110 | // Our backend will assume that no params means a call to support_agent 111 | device.connect(); 112 | }; 113 | 114 | /* End a call */ 115 | window.hangUp = function() { 116 | device.disconnectAll(); 117 | }; 118 | -------------------------------------------------------------------------------- /resources/sass/_variables.scss: -------------------------------------------------------------------------------- 1 | // Body 2 | $body-bg: #f8fafc; 3 | 4 | // Typography 5 | $font-family-sans-serif: 'Nunito', sans-serif; 6 | $font-size-base: 0.9rem; 7 | $line-height-base: 1.6; 8 | 9 | // Colors 10 | $blue: #3490dc; 11 | $indigo: #6574cd; 12 | $purple: #9561e2; 13 | $pink: #f66d9b; 14 | $red: #e3342f; 15 | $orange: #f6993f; 16 | $yellow: #ffed4a; 17 | $green: #38c172; 18 | $teal: #4dc0b5; 19 | $cyan: #6cb2eb; 20 | -------------------------------------------------------------------------------- /resources/sass/app.scss: -------------------------------------------------------------------------------- 1 | // Fonts 2 | @import url('https://fonts.googleapis.com/css?family=Nunito'); 3 | 4 | // Variables 5 | @import 'variables'; 6 | 7 | // Bootstrap 8 | @import '~bootstrap/scss/bootstrap'; 9 | 10 | @import 'bicycle-polo'; 11 | -------------------------------------------------------------------------------- /resources/sass/bicycle-polo.scss: -------------------------------------------------------------------------------- 1 | /* Custom CSS for Bicycle Polo example */ 2 | 3 | .bicycle-polo-background { 4 | background-image: url("../img/bicycle-polo.jpg"); 5 | background-size: cover; 6 | background-position: 75% 25%; 7 | } 8 | 9 | .bicycle-polo-text { 10 | background-color: rgba(255, 255, 255, 0.75); 11 | padding-bottom: 1rem; 12 | 13 | h1 { 14 | font-weight: bold; 15 | } 16 | 17 | @media (min-width: 768px) { 18 | margin-right: 5%; 19 | width: 40%; 20 | } 21 | } 22 | 23 | // .bicycle-polo-text h1 { 24 | // font-weight: bold; 25 | // } 26 | 27 | // @media (min-width: 768px) { 28 | // .bicycle-polo-text { 29 | // margin-right: 5%; 30 | // width: 40%; 31 | // } 32 | // } 33 | 34 | .answer-button, 35 | .call-support-button { 36 | width: 48%; 37 | float: left; 38 | } 39 | 40 | .hangup-button { 41 | width: 48%; 42 | float: right; 43 | } 44 | 45 | .container { 46 | padding-top: 15px; 47 | } 48 | -------------------------------------------------------------------------------- /resources/views/_messages.blade.php: -------------------------------------------------------------------------------- 1 | @if (count($errors) > 0) 2 | 8 | @endif 9 | 10 | @if (session('status') !== null) 11 | 15 | @endif 16 | -------------------------------------------------------------------------------- /resources/views/errors/503.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Be right back. 5 | 6 | 7 | 8 | 39 | 40 | 41 |
42 |
43 |
Be right back.
44 |
45 |
46 | 47 | 48 | -------------------------------------------------------------------------------- /resources/views/index.blade.php: -------------------------------------------------------------------------------- 1 | @extends('layouts.master') 2 | 3 | @section('title') 4 | Home 5 | @endsection 6 | 7 | @section('content') 8 | 9 |
10 |
11 |

Birchwood Bicycle Polo Co.

12 |

13 | We sell only the finest bicycle polo supplies for those seeking fame and glory in this sport of kings. 14 |

15 |
16 |

Having trouble with one of our products?

17 |

18 | Talk to one of our support agents now — or fill out the support form below and someone will call you later. 19 |

20 | Get help 21 |
22 |
23 | 24 |
25 | 26 | @include('_messages') 27 | 28 |

Contact support

29 | 30 |

31 | Talk with one of our support agents right now by clicking the "Call Support" button on the right. If you can't talk now, fill out a support ticket and an agent will call you later. 32 |

33 | 34 |
35 | 36 |
37 |
38 |
39 | Talk to support now 40 |
41 |
42 |
43 | 44 |
45 | 46 |
47 |
48 | 51 | 52 |
53 |
54 |
55 | 56 |
57 |
58 | @csrf 59 |
60 | 61 | 62 |
63 |
64 | 65 | 66 |
67 |
68 | 69 | 70 |
71 |
72 | 73 |
74 |
75 |
76 |
77 |
78 | @endsection 79 | 80 | @section('footer') 81 | 82 | Header photo courtesy David Sachs via Flickr 83 | 84 | @endsection 85 | -------------------------------------------------------------------------------- /resources/views/layouts/master.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | @yield('title', 'Calls') - Browser Calls 7 | 8 | 9 | 10 | 11 | 14 | 15 | 16 | 17 | 18 | @yield('css') 19 | 20 | 21 | 22 | 23 | 44 | 45 | @yield('content') 46 | 47 |
48 |
49 |

© Your Company 2020

50 | @yield('footer') 51 |
52 |
53 | 54 | 55 | 56 | @yield('javascript') 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /resources/views/supportDashboard.blade.php: -------------------------------------------------------------------------------- 1 | @extends('layouts.master') 2 | 3 | @section('title') 4 | Support Dashboard 5 | @endsection 6 | 7 | @section('content') 8 |
9 |
10 |
11 |

Support Tickets

12 | 13 |

14 | This is the list of most recent support tickets. Click the "Call customer" button to start a phone call from your browser. 15 |

16 |
17 |
18 | 19 | 20 |
21 | 22 |
23 |
24 |
25 | Make a call 26 |
27 |
28 |
29 | 30 |
31 | 32 |
33 |
34 | 35 | 36 |
37 |
38 |
39 | 40 |
41 | @foreach ($tickets as $ticket) 42 |
43 |
44 | Ticket #{{ $ticket->id }} 45 | {{ $ticket->created_at}} 46 |
47 | 48 |
49 |
50 |
51 |

Name: {{ $ticket->name }}

52 |

Phone number: {{ $ticket->phone_number }}

53 |

Description:

54 | {{ $ticket->description }} 55 |
56 | 57 |
58 | 61 |
62 |
63 |
64 |
65 | @endforeach 66 |
67 | 68 |
69 |
70 | @endsection('content') 71 | -------------------------------------------------------------------------------- /routes/channels.php: -------------------------------------------------------------------------------- 1 | id === (int) $id; 18 | }); 19 | -------------------------------------------------------------------------------- /routes/console.php: -------------------------------------------------------------------------------- 1 | comment(Inspiring::quote()); 19 | })->describe('Display an inspiring quote'); 20 | -------------------------------------------------------------------------------- /routes/web.php: -------------------------------------------------------------------------------- 1 | 'home', function () { 22 | return response()->view('index'); 23 | }] 24 | ); 25 | Route::post( 26 | '/token', 27 | ['uses' => 'TokenController@newToken', 'as' => 'new-token'] 28 | ); 29 | Route::get( 30 | '/dashboard', 31 | ['uses' => 'DashboardController@dashboard', 'as' => 'dashboard'] 32 | ); 33 | Route::post( 34 | '/ticket', 35 | ['uses' => 'TicketController@newTicket', 'as' => 'new-ticket'] 36 | ); 37 | Route::post( 38 | '/support/call', 39 | ['uses' => 'CallController@newCall', 'as' => 'new-call'] 40 | ); 41 | -------------------------------------------------------------------------------- /server.php: -------------------------------------------------------------------------------- 1 | 8 | */ 9 | 10 | $uri = urldecode( 11 | parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH) 12 | ); 13 | 14 | // This file allows us to emulate Apache's "mod_rewrite" functionality from the 15 | // built-in PHP web server. This provides a convenient way to test a Laravel 16 | // application without having installed a "real" web server software here. 17 | if ($uri !== '/' && file_exists(__DIR__.'/public'.$uri)) { 18 | return false; 19 | } 20 | 21 | require_once __DIR__.'/public/index.php'; 22 | -------------------------------------------------------------------------------- /storage/app/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !public/ 3 | !.gitignore 4 | -------------------------------------------------------------------------------- /storage/app/public/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/.gitignore: -------------------------------------------------------------------------------- 1 | config.php 2 | routes.php 3 | schedule-* 4 | compiled.php 5 | services.json 6 | events.scanned.php 7 | routes.scanned.php 8 | down 9 | -------------------------------------------------------------------------------- /storage/framework/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !data/ 3 | !.gitignore 4 | -------------------------------------------------------------------------------- /storage/framework/cache/data/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/sessions/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/testing/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/framework/views/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /storage/logs/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /tests/CreatesApplication.php: -------------------------------------------------------------------------------- 1 | make(Kernel::class)->bootstrap(); 19 | 20 | return $app; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tests/Feature/CallControllerTest.php: -------------------------------------------------------------------------------- 1 | call( 17 | 'POST', 18 | route('new-call'), 19 | ['phoneNumber' => $fakeCustomerNumber] 20 | ); 21 | $responseDocument = new \SimpleXMLElement($response->getContent()); 22 | 23 | // Then 24 | $this->assertNotNull($responseDocument->Dial); 25 | $this->assertNotNull($responseDocument->Dial->Number); 26 | $this->assertEquals( 27 | $fakeCustomerNumber, 28 | $responseDocument->Dial->Number 29 | ); 30 | $this->assertEquals( 31 | $responseDocument->Dial->attributes()['callerId'], 32 | config('services.twilio')['number'] 33 | ); 34 | } 35 | 36 | public function testNewCallWithoutPhoneNumber() 37 | { 38 | // When 39 | $response = $this->call( 40 | 'POST', 41 | route('new-call') 42 | ); 43 | $responseDocument = new \SimpleXMLElement($response->getContent()); 44 | 45 | // Then 46 | $this->assertNotNull($responseDocument->Dial); 47 | $this->assertNotNull($responseDocument->Dial->Client); 48 | $this->assertEquals('support_agent', $responseDocument->Dial->Client); 49 | $this->assertEquals( 50 | $responseDocument->Dial->attributes()['callerId'], 51 | config('services.twilio')['number'] 52 | ); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /tests/Feature/ExampleTest.php: -------------------------------------------------------------------------------- 1 | get('/'); 18 | 19 | $response->assertStatus(200); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /tests/Feature/TicketControllerTest.php: -------------------------------------------------------------------------------- 1 | startSession(); 17 | $validPhoneNumber = '+15558180101'; 18 | $this->assertCount(0, Ticket::all()); 19 | 20 | // When 21 | $response = $this->call( 22 | 'POST', 23 | route('new-ticket'), 24 | ['name' => 'Customer name', 25 | 'phone_number' => $validPhoneNumber, 26 | 'description' => 'I lost my ability to even', 27 | '_token' => csrf_token()] 28 | ); 29 | 30 | // Then 31 | $this->assertCount(1, Ticket::all()); 32 | $ticket = Ticket::first(); 33 | 34 | $this->assertEquals($ticket->name, 'Customer name'); 35 | $this->assertEquals($ticket->phone_number, $validPhoneNumber); 36 | $this->assertEquals($ticket->description, 'I lost my ability to even'); 37 | 38 | $response->assertRedirect(route('home')); 39 | $response->assertSessionHas('status'); 40 | 41 | $flashMessage = $this->app['session']->get('status'); 42 | $this->assertEquals( 43 | $flashMessage, 44 | "We've received your support ticket. We'll be in touch soon!" 45 | ); 46 | } 47 | 48 | public function testPhoneNumberValidation() 49 | { 50 | // Given 51 | $this->startSession(); 52 | $invalidPhoneNumber = '5558180'; 53 | 54 | // When 55 | $response = $this->call( 56 | 'POST', 57 | route('new-ticket'), 58 | ['name' => 'Customer name', 59 | 'phone_number' => $invalidPhoneNumber, 60 | 'description' => 'I lost my ability to even', 61 | '_token' => csrf_token()] 62 | ); 63 | 64 | // Then 65 | $flashMessage = $this 66 | ->app['session'] 67 | ->get('errors') 68 | ->getBag('default') 69 | ->get('phone_number'); 70 | 71 | $this->assertEquals( 72 | ['The phone number must be in E.164 format'], 73 | $flashMessage 74 | ); 75 | $this->assertCount(0, Ticket::all()); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /tests/Feature/TokenControllerTest.php: -------------------------------------------------------------------------------- 1 | makePartial(); 21 | $mockVoiceGrant 22 | ->shouldReceive('setOutgoingApplicationSid') 23 | ->with($applicationSid); 24 | $mockVoiceGrant 25 | ->shouldReceive('setIncomingAllow') 26 | ->with(true); 27 | 28 | $mockTwilioCapability = Mockery::mock(AccessToken::class) 29 | ->makePartial(); 30 | $mockTwilioCapability 31 | ->shouldReceive('addGrant') 32 | ->with($mockVoiceGrant); 33 | 34 | $mockTwilioCapability 35 | ->shouldReceive('toJWT') 36 | ->andReturn($mockToken); 37 | 38 | $this->app->instance( 39 | AccessToken::class, 40 | $mockTwilioCapability 41 | ); 42 | 43 | // When 44 | $newTokenResponse = $this->call( 45 | 'POST', 46 | route('new-token'), 47 | ['forPage' => route('dashboard', [], false)] 48 | ); 49 | 50 | // Then 51 | $newToken = json_decode($newTokenResponse->getContent()); 52 | 53 | $newTokenResponse->assertSee('token', true); 54 | $this->assertEquals( 55 | $mockToken, 56 | $newToken->token 57 | ); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | assertTrue(true); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /webpack.mix.js: -------------------------------------------------------------------------------- 1 | const mix = require('laravel-mix'); 2 | 3 | /* 4 | |-------------------------------------------------------------------------- 5 | | Mix Asset Management 6 | |-------------------------------------------------------------------------- 7 | | 8 | | Mix provides a clean, fluent API for defining some Webpack build steps 9 | | for your Laravel application. By default, we are compiling the Sass 10 | | file for the application as well as bundling up all the JS files. 11 | | 12 | */ 13 | 14 | mix.js('resources/js/browser-calls.js', 'public/js') 15 | .extract(['twilio-client']) 16 | .sass('resources/sass/app.scss', 'public/css'); 17 | --------------------------------------------------------------------------------